fix(scope): close final ownership races

Drain idle injection flushes before agent teardown, snapshot approval and subagent provider inputs, and gate subagent lifecycle events on real child readiness. Align the RFCs and generated contracts with the hardened behavior.
This commit is contained in:
Tianyi Cui
2026-07-12 00:31:46 +08:00
parent e6c7522a75
commit 3529b3c166
43 changed files with 825 additions and 261 deletions

View File

@@ -6,7 +6,7 @@ The run mechanics live in the shared [`@deepseek-ai/dsh-subagent-inprocess`](../
## What it does
`start(request)` delegates to `startInProcessRun(ctx, request, {})` with no seed: a fresh child agent with the parent's `cwd`/`parentSession` lineage and (by default) the parent's model. The driver creates one run-owner fiber under `parent.ctx`; parent teardown, this provider's teardown, and manual disposal all converge there before child publication. See the [driver README](../subagent-inprocess/README.md) for the full lifecycle (depth check, one-shot drive, result read, dispose).
`start(request)` delegates to `startInProcessRun(ctx, request, {})` with no seed: a fresh child agent with the parent's `cwd`/`parentSession` lineage and (by default) the parent's model. The driver creates one run-owner fiber under `parent.ctx`; parent teardown, this provider's teardown, and manual disposal all converge there before child publication. Its `run.started` boundary resolves only after the fresh child is published, so `subagent/start` observers see a live registry entry. See the [driver README](../subagent-inprocess/README.md) for the full lifecycle (depth check, one-shot drive, result read, dispose).
## Capabilities

View File

@@ -55,6 +55,25 @@ describe('dsh-subagent-spawn', () => {
await run.dispose()
})
it('emits subagent/start only after the fresh child is published', async () => {
const { ctx, parent } = await setup([textResponse('child answer')])
let childAtStart: ReturnType<typeof ctx.agents.get>
ctx.on('subagent/start', (info) => {
if (info.provider === 'spawn') childAtStart = ctx.agents.get(info.id)
})
const run = ctx.subagents.start('spawn', { prompt: [{ type: 'text', text: 'do X' }], parent })
// Creation is asynchronous; no lifecycle claim is made while the child is
// still inside its unpublished setup transaction.
expect(childAtStart).toBeUndefined()
await run.started
expect(childAtStart).toBe(ctx.agents.get(run.id))
expect(childAtStart?.id).toBe(run.id)
await run.result
await run.dispose()
})
it('gives the child its OWN session (not the parent\'s), with parentSession lineage', async () => {
const { ctx, parent } = await setup([textResponse('hi')])
const run = ctx.subagents.start('spawn', { prompt: [{ type: 'text', text: 'p' }], parent })