fix(browser): mark killed/failed job_output status lines as failed
job_output always terminates its read with the tool-jobs status line [status: killed] / [status: failed, detail], and a background job that dies settles with isError false — so the read showed a green OK for a dead job. tailStatusOf now treats the two terminal failure statuses as failures; completed/running/stopping stay notices. This goes beyond the official chat UI, which renders job_output as a generic card and shows OK too.
This commit is contained in:
1 parent
3f37450a0f
commit
79950c9822
2 files changed
+64
-1
No files matched your search
@@ -362,8 +362,11 @@ function callSummaryOf(conv: ConversationNodeLike | undefined): string | null {
|
||||
* - persistent shells: `[shell killed by signal: X]`, `[shell exited: code N]` — riding LAST, after the
|
||||
* `[exit code: N]` of the command whose failure killed the shell, so the command marker is re-checked
|
||||
* on the preceding text
|
||||
* - job_output: `[status: killed]` / `[status: failed, detail]` — the tool-jobs status line always terminates
|
||||
* the read; a killed/failed background job settles with `isError` false, so only the line flags the loss
|
||||
* End-anchored like dsh's own parseExitStatus, so marker text quoted inside the output (e.g. a cat'ed log)
|
||||
* is not a failure. A clean shell exit (code 0 or code-less) is a session-loss notice, not a failure.
|
||||
* is not a failure. A clean shell exit (code 0 or code-less) or a live/completed job status is a notice,
|
||||
* not a failure.
|
||||
* The parsed exit code feeds the FAILED run-state pill.
|
||||
*/
|
||||
function tailStatusOf(conv: ConversationNodeLike | undefined): { fail: boolean; exit: number | null } {
|
||||
@@ -383,6 +386,7 @@ function tailStatusOf(conv: ConversationNodeLike | undefined): { fail: boolean;
|
||||
const exit = /\[exit code:\s*(\d+)\]$/.exec(tail)
|
||||
if (exit !== null) return { fail: true, exit: Number(exit[1]) }
|
||||
if (/\[killed by signal: [^\]\n]+\]$/.test(tail)) return { fail: true, exit: null }
|
||||
if (/\[status: (?:killed|failed)(?:, [^\]\n]*)?\]$/.test(tail)) return { fail: true, exit: null }
|
||||
}
|
||||
return { fail: false, exit: null }
|
||||
}
|
||||
|
||||
@@ -326,3 +326,62 @@ test('tool result: persistent-shell death markers mark the run FAILED, a clean s
|
||||
|
||||
console.log('✔ persistent-shell marker test passed (shell death fails, clean exit stays a notice)')
|
||||
})
|
||||
|
||||
test('tool result: a terminal job_output status line marks a killed/failed background job FAILED', async () => {
|
||||
bed.dataValue = {
|
||||
...snapshot,
|
||||
nodes: [
|
||||
...snapshot.nodes,
|
||||
// job_output always ends its read with the tool-jobs `[status: ...]` line; killed/failed settle with
|
||||
// isError false, so the line is the only failure signal.
|
||||
{ seq: 3, cat: 'tool', tool: 'job_output', tokens: 8, time: 66000 },
|
||||
{ seq: 4, cat: 'tool', tool: 'job_output', tokens: 8, time: 67000 },
|
||||
{ seq: 5, cat: 'tool', tool: 'job_output', tokens: 8, time: 68000 },
|
||||
{ seq: 6, cat: 'tool', tool: 'job_output', tokens: 8, time: 69000 },
|
||||
],
|
||||
}
|
||||
bed.useSessionHolder = (sel) => sel({
|
||||
nodes: [
|
||||
{
|
||||
kind: 'tool-result', seq: 3,
|
||||
call: { name: 'job_output', argsRaw: '{"job_id":"j1"}' },
|
||||
content: [{ type: 'text', text: 'partial output\n[status: killed]' }],
|
||||
},
|
||||
{
|
||||
kind: 'tool-result', seq: 4,
|
||||
call: { name: 'job_output', argsRaw: '{"job_id":"j2"}' },
|
||||
content: [{ type: 'text', text: 'boom\n[status: failed, exited 1]' }],
|
||||
},
|
||||
{
|
||||
kind: 'tool-result', seq: 5,
|
||||
call: { name: 'job_output', argsRaw: '{"job_id":"j3"}' },
|
||||
content: [{ type: 'text', text: 'all done\n[status: completed]' }],
|
||||
},
|
||||
{
|
||||
kind: 'tool-result', seq: 6,
|
||||
call: { name: 'job_output', argsRaw: '{"job_id":"j4","wait":true}' },
|
||||
content: [{ type: 'text', text: '(no new output)\n[status: running]' }],
|
||||
},
|
||||
],
|
||||
})
|
||||
brSlots[1][1]('tool')
|
||||
let tr = renderView()
|
||||
const toolRows = byClass(tr, 'lc-br-elem-row')
|
||||
assert.equal(toolRows.length, 4, 'all four job_output reads listed (newest first)')
|
||||
assert.equal(byClass(toolRows[0], 'lc-br-err-dot').length, 0, 'running job stays a notice')
|
||||
assert.equal(byClass(toolRows[1], 'lc-br-err-dot').length, 0, 'completed job carries no error dot')
|
||||
assert.equal(byClass(toolRows[2], 'lc-br-err-dot').length, 1, 'failed job carries the red error dot')
|
||||
assert.equal(byClass(toolRows[3], 'lc-br-err-dot').length, 1, 'killed job carries the red error dot')
|
||||
|
||||
brSlots[2][1]('n4')
|
||||
tr = renderView()
|
||||
const errPill = byClass(tr, 'lc-ts-call-err')[0]
|
||||
assert.ok(errPill, 'failed job read carries the red Failed pill')
|
||||
assert.doesNotMatch(textOf(errPill), /exit/, 'a job status line carries no exit code for the pill')
|
||||
|
||||
brSlots[2][1]('n5')
|
||||
tr = renderView()
|
||||
assert.ok(byClass(tr, 'lc-ts-call-ok')[0], 'completed job read keeps the green OK pill')
|
||||
|
||||
console.log('✔ job status-line test passed (killed/failed fail, completed/running stay notices)')
|
||||
})
|
||||
Reference in new issue
Block a user