fix: keep bash status rendering and parsing together
This commit is contained in:
@@ -69,7 +69,7 @@ import type {} from '@deepseek-ai/dsh-user-approval'
|
|||||||
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
import type { SandboxMode } from '@deepseek-ai/dsh-sandbox'
|
||||||
import { BashTaskId, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash'
|
import { BashTaskId, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash'
|
||||||
import type { BashTask } from '@deepseek-ai/dsh-bash'
|
import type { BashTask } from '@deepseek-ai/dsh-bash'
|
||||||
import { renderResult } from './render.ts'
|
import { parseExitStatus, renderResult } from './render.ts'
|
||||||
|
|
||||||
export const name = 'tool-bash'
|
export const name = 'tool-bash'
|
||||||
export const inject = ['tools', 'bash', 'systemPrompt']
|
export const inject = ['tools', 'bash', 'systemPrompt']
|
||||||
@@ -275,39 +275,6 @@ function presentBashResult(args: unknown, result: ToolResult): ToolResultView |
|
|||||||
return { card: 'terminal', output: raw, ...parseExitStatus(raw) }
|
return { card: 'terminal', output: raw, ...parseExitStatus(raw) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Recover the structured exit status from a rendered `renderResult` string — the
|
|
||||||
* inverse of the status markers it appends. A `[killed by signal: SIG]` marker
|
|
||||||
* yields `{signal}`; otherwise an `[exit code: N]` marker yields `{exitCode:N}`;
|
|
||||||
* absent both we report `{exitCode:0}` (a clean run appends no marker — and a
|
|
||||||
* trapped-timeout run that exits 0 also has none and is accurately exit 0).
|
|
||||||
*
|
|
||||||
* Why parse rendered text at all: `presentResult` is replay-safe and on a
|
|
||||||
* `session/load` the ONLY thing persisted is this content text — the structured
|
|
||||||
* `BashRunResult` is long gone — so unless the exit were added to the persisted
|
|
||||||
* event schema (deliberately NOT done; see the terminal-rendering RFC), parsing
|
|
||||||
* is the only channel. The match is anchored to a LEADING newline + end-of-string
|
|
||||||
* because `renderResult` always inserts a `\n` before the marker (line ~124) onto
|
|
||||||
* a non-empty body: a real marker is therefore always its own final line. That
|
|
||||||
* defeats the common spoof (program output that simply ENDS in `[exit code: 5]`
|
|
||||||
* with no trailing newline — a clean exit 0 — no longer reads as a failure).
|
|
||||||
*
|
|
||||||
* KNOWN RESIDUAL (inherent to the replay-only-sees-text design): a clean exit 0
|
|
||||||
* whose body's FINAL line is itself exactly the marker text — `[exit code: N]`
|
|
||||||
* or `[killed by signal: SIG]`, printed by the program with nothing after — is
|
|
||||||
* still indistinguishable from a real marker and would show a wrong pill. This is
|
|
||||||
* display-only (execution and the model-facing text are unaffected) and narrow;
|
|
||||||
* the complete fix is to persist a structured exit on the result event, which the
|
|
||||||
* RFC names as the escape hatch.
|
|
||||||
*/
|
|
||||||
function parseExitStatus(text: string): { exitCode: number } | { signal: string } {
|
|
||||||
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
|
|
||||||
if (signal?.[1] !== undefined) return { signal: signal[1] }
|
|
||||||
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
|
|
||||||
if (exit?.[1] !== undefined) return { exitCode: Number(exit[1]) }
|
|
||||||
return { exitCode: 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Pending-state presentation for `bash_output`/`bash_kill` (background-task tools). */
|
/** Pending-state presentation for `bash_output`/`bash_kill` (background-task tools). */
|
||||||
function presentTaskCall(verb: string, args: { task_id: string }): GenericCallView {
|
function presentTaskCall(verb: string, args: { task_id: string }): GenericCallView {
|
||||||
return { card: 'generic', title: `${verb} background task ${args.task_id}`, kind: 'execute', rawInput: args.task_id }
|
return { card: 'generic', title: `${verb} background task ${args.task_id}`, kind: 'execute', rawInput: args.task_id }
|
||||||
|
|||||||
@@ -68,3 +68,25 @@ export function renderResult(
|
|||||||
if (!body.endsWith('\n')) body += '\n'
|
if (!body.endsWith('\n')) body += '\n'
|
||||||
return body + markers.join('\n')
|
return body + markers.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recover the structured exit status from a rendered {@link renderResult}
|
||||||
|
* string — the inverse of the status markers it appends. A killed marker
|
||||||
|
* yields `signal`; otherwise a non-zero marker yields `exitCode`; absent both
|
||||||
|
* means a clean exit 0.
|
||||||
|
*
|
||||||
|
* Replay only retains the rendered content text, not the original
|
||||||
|
* `BashRunResult`, so terminal presentation must recover the exit pill here.
|
||||||
|
* Requiring a leading newline and the end of the string keeps ordinary output
|
||||||
|
* that merely ends with marker-like text from matching unless the final line
|
||||||
|
* is indistinguishable from a real marker.
|
||||||
|
* @param text - rendered model-facing bash result.
|
||||||
|
* @returns the recovered terminal exit code or signal.
|
||||||
|
*/
|
||||||
|
export function parseExitStatus(text: string): { exitCode: number } | { signal: string } {
|
||||||
|
const signal = /\n\[killed by signal: ([^\]\n]+)\]$/.exec(text)
|
||||||
|
if (signal?.[1] !== undefined) return { signal: signal[1] }
|
||||||
|
const exit = /\n\[exit code: (\d+)\]$/.exec(text)
|
||||||
|
if (exit?.[1] !== undefined) return { exitCode: Number(exit[1]) }
|
||||||
|
return { exitCode: 0 }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user