fix(subagent): preserve startup cleanup failures

This commit is contained in:
pku-xht
2026-08-12 17:38:06 +08:00
parent 28fcda2751
commit caf7d48f88
11 changed files with 59 additions and 15 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-claude-code/README.md
README.md: 556cde7da4467659d02709c43325e60fa7de6a74
README.zh.md: bda00d7be4467e739604eeba0020850cb1a99d50
README.md: f0f6e39820920e0b266802585b74a953a068828a
README.zh.md: 52baee6c1158bb38505fd0cf454a820853e0cf89

View File

@@ -82,7 +82,7 @@ Independent of the parent request cache. Reuse depends only on Claude Code's own
#### What the model sees
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the strict final Claude Code answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Claude Code reasoning, tool activity, intermediate messages, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
#### Token effect

View File

@@ -82,7 +82,7 @@ Claude Code 子任务会在一个全新的 SDK query 中接收独立文本任务
#### 模型看到的内容
通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
通过 `dsh-tool-subagent`,前台调用会让父级模型看到符合严格成功条件的 Claude Code 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id随后通用任务控制面会送达完成通知通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Claude Code 的推理、工具活动、中间消息、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
#### 对 token 的影响

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-codex/README.md
README.md: 6a882c128ebd372ee148a4315a237d4eacf6d42b
README.zh.md: beb5411f6a449efc7a45c171d217f98657805740
README.md: 570a2346b121a3d100600285f225a0906c38aeec
README.zh.md: 9fded2ceb35b276f0c698c937720ea13e13c6acc

View File

@@ -76,7 +76,7 @@ Independent of the parent request cache. Reuse depends only on Codex's own provi
#### What the model sees
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a bounded completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
Through `dsh-tool-subagent`, a foreground call gives the parent the selected final Codex answer or the consumer's exact error for a non-completed result. A background call first returns a Task id; the generic task controls later deliver a completion notice, expose the final answer and status through `task_output`, and let `task_kill` request cancellation. Codex commentary, reasoning, tool activity, stderr, workspace diffs, usage, and product ids are not copied into the parent Session.
#### Token effect

View File

@@ -76,7 +76,7 @@ Codex 子任务会在一个全新的临时线程中,以单个轮次接收这
#### 模型看到的内容
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id随后通用任务控制面会送达有界完成通知,通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理reasoning、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
通过 `dsh-tool-subagent`,前台调用会让父级模型看到选定的 Codex 最终答案,或者在结果未完成时看到消费方给出的原样错误。后台调用会先返回 Task id随后通用任务控制面会送达完成通知通过 `task_output` 公开最终答案与状态,并允许 `task_kill` 请求取消。Codex 的过程说明、推理reasoning、工具活动、stderr、工作区差异、用量信息和产品标识符均不会复制到父会话。
#### 对 token 的影响

View File

@@ -113,7 +113,9 @@ async function settleStart(start: Promise<SubagentRun>, signal: AbortSignal): Pr
try {
return await settleRun(await start)
} catch (error: unknown) {
return signal.aborted
// Product providers aggregate startup and rollback failures. Cancellation
// must not turn a failed cleanup into a cleanly killed Task.
return signal.aborted && !(error instanceof AggregateError)
? { status: 'killed' }
: { status: 'failed', detail: String(error) }
}

View File

@@ -939,6 +939,48 @@ describe('dsh-tool-subagent background mode', () => {
expect(text(output)).toBe('(no new output)\n[status: killed]')
})
it('reports startup rollback failure after cancellation as a failed task', async () => {
const ctx = await backgroundSetup({ provider: 'mock' })
const parent = ownerAgent(ctx, 'sess-parent')
ctx.subagents.registerProvider({
name: 'broken-start-rollback',
capabilities: { outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
start: request => new Promise((_resolve, reject) => {
request.signal.addEventListener('abort', () => {
reject(new AggregateError(
[new Error('startup aborted'), new Error('cleanup failed')],
'startup failed and cleanup also failed',
))
}, { once: true })
}),
})
tool.apply(ctx, { provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' })
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-start'),
name: 'subagent_broken_rollback',
arguments: { description: 'broken rollback', prompt: 'p', run_in_background: true },
agent: parent,
})
await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-kill'),
name: 'task_kill',
arguments: { task_id: 'subagent-1' },
agent: parent,
})
const output = await ctx.tools.execute({
signal: testToolSignal,
callId: CallId('broken-rollback-output'),
name: 'task_output',
arguments: { task_id: 'subagent-1', wait: true },
agent: parent,
})
expect(text(output)).toContain('[status: failed, AggregateError: startup failed and cleanup also failed]')
})
it('forwards task_kill reasons through the run signal (and defaults one when absent)', async () => {
// Use a provider that remains live until its signal is aborted.
const ctx = await backgroundSetup({ provider: 'mock', agentOptions: { model: 'child-model' } })