refactor(tasks): declare-then-execute — ctx.tasks.start() replaces register()

start({ kind, label, owner, run }) preflights everything that can fail
(the attachSurface fence, validation, the owner-cleanup attach) BEFORE
invoking the producer's run() starter, then commits atomically —
'work started but never got a collectable id' is now structurally
impossible instead of a producer try/catch rollback obligation (the
P1 review fix, rebuilt on #185's declare/execute split). Producers
lose their catch-wraps; the leak tests now pin the stronger property
that a failed preflight never spawns anything. TaskRegistration splits
into TaskStart (identity + run) and TaskHooks (cancel/done/readOutput);
docs, type-equiv manifest, catalogs, and both RFCs move with it.
This commit is contained in:
Yichen Jiang
2026-07-09 21:55:07 +08:00
parent f858c647a6
commit bd59fddacd
22 files changed
+281 -240

No files matched your search

+20 -25
View File
@@ -264,32 +264,27 @@ export function apply(ctx: Context, config: Config): void {
// (the child outlives this step; cancellation belongs to task_kill
// and owner-disposal cleanup), so the request carries NO signal.
if (exec.signal?.aborted) throw new Error('subagent delegation aborted')
const run = ctx.subagents.start(config.provider, {
prompt: [{ type: 'text', text: args.prompt }],
parent,
...config.agentOptions ? { agentOptions: config.agentOptions } : {},
// tasks.start preflights (surface fence, owner cleanup) BEFORE run()
// spawns the child, and cannot fail after — a child can never start
// without a collectable id.
const id = tasks.start({
kind: 'subagent',
label: args.description,
owner: parent,
run: () => {
const run = ctx.subagents.start(config.provider, {
prompt: [{ type: 'text', text: args.prompt }],
parent,
...config.agentOptions ? { agentOptions: config.agentOptions } : {},
})
return {
cancel: (reason?: string) => { run.cancel(reason ?? 'background subagent task killed') },
done: settleRun(run),
// No readOutput: a subagent task is final-output-only — the
// child session remains the detailed trace.
}
},
})
const done = settleRun(run)
let id: string
try {
id = tasks.register({
kind: 'subagent',
label: args.description,
owner: parent,
cancel: (reason) => { run.cancel(reason ?? 'background subagent task killed') },
done,
// No readOutput: a subagent task is final-output-only — the child
// session remains the detailed trace.
})
} catch (error: unknown) {
// A failed registration must not leak the just-started child: the
// model never received an id, so nothing could ever task_kill it.
// Cancel, await `done` (which settles only after run.dispose() —
// child quiescence), then fail the call with the real cause.
run.cancel('background task registration failed')
await done
throw error
}
return [{ type: 'text', text: `started background subagent task ${id}` }]
}