diff --git a/packages/bash/bash-local/src/index.ts b/packages/bash/bash-local/src/index.ts index 1ad81d9014..61cb7cf617 100644 --- a/packages/bash/bash-local/src/index.ts +++ b/packages/bash/bash-local/src/index.ts @@ -175,10 +175,12 @@ export class LocalBashExecutor extends BashExecutor { exitCode: null, signal: null, done: running.done.then((outcome) => { - // Abort-killed processes report as killed, not completed. Background - // runs forward only the upstream signal (no timeout), so its aborted - // state is the authoritative "was this cancelled" signal. - if (proc.status === 'running') proc.status = spec.signal?.aborted === true ? 'killed' : 'completed' + // Caller-aborted and signal-terminated processes report as killed, not + // completed. The signal check also covers commands that terminate + // themselves without aborting the upstream signal. + if (proc.status === 'running') { + proc.status = spec.signal?.aborted === true || outcome.signal !== null ? 'killed' : 'completed' + } proc.exitCode = outcome.exitCode proc.signal = outcome.signal this.live.delete(proc) diff --git a/packages/bash/bash-local/tests/executor.spec.ts b/packages/bash/bash-local/tests/executor.spec.ts index fa9bb23f5f..e2d799f5ae 100644 --- a/packages/bash/bash-local/tests/executor.spec.ts +++ b/packages/bash/bash-local/tests/executor.spec.ts @@ -249,6 +249,15 @@ describe('LocalBashExecutor.start (background process handles)', () => { expect(proc.signal).toBe('SIGTERM') }) + it('a self-signal exit settles the handle as killed, not completed', async () => { + const { bash } = await setup() + const proc = bash.start(bash.resolve({ command: 'kill -TERM $$' })) + await proc.done + expect(proc.status).toBe('killed') + expect(proc.exitCode).toBeNull() + expect(proc.signal).toBe('SIGTERM') + }) + it('a background spawn failure settles as killed with the error readable on stderr', async () => { const { bash } = await setup() const proc = bash.start(bash.resolve({ command: 'true', workdir: '/nonexistent-dsh' })) diff --git a/packages/bash/tool-bash/tests/tools.spec.ts b/packages/bash/tool-bash/tests/tools.spec.ts index 23e2dac95a..30d71e1d61 100644 --- a/packages/bash/tool-bash/tests/tools.spec.ts +++ b/packages/bash/tool-bash/tests/tools.spec.ts @@ -288,6 +288,14 @@ describe('background execution through the task runtime', () => { expect(text(final)).toContain('[status: killed, signal: SIGTERM]') }) + it('a self-signal background exit is reported as killed through the REAL task_output tool', async () => { + const ctx = await setupWithTasks() + await call(ctx, 'bash', { command: 'kill -TERM $$', description: 'test command', run_in_background: true }) + + const final = await call(ctx, 'task_output', { task_id: 'bash-1', wait: true }) + expect(text(final)).toContain('[status: killed, signal: SIGTERM]') + }) + it('a background task started by an agent is registered with that agent as owner', async () => { // The fence SEMANTICS are pinned in dsh-tasks; this only pins that // tool-bash forwards exec.agent as the registration's owner.