subagent-inprocess: stop the structured nudge loop once cancellation lands
A cancel that lands AFTER a child's clean turn end but before the nudge continuation runs clears nothing — child.cancel() only kills queued or running work — so the loop's turn-state check alone let a later child.send() spend a fresh post-cancellation turn (and even capture a structured result the caller had already abandoned). The loop condition now also reads the run's own cancelled flag, re-evaluated after every whenIdle(), through an accessor because closure assignments are invisible to control-flow narrowing (an inline read lints always-true). readResult keeps the outcome honest on this path: a completed-but- uncaptured turn maps to aborted, not error, when a cancel is why the nudging stopped — the cancel contract outranks the schema shortfall.
This commit is contained in:
@@ -179,6 +179,25 @@ describe('in-process structured output', () => {
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('a cancel landing after a clean turn end stops the nudge loop: no post-cancellation turn is spent', async () => {
|
||||
const { ctx, parent, adapter } = await setup([textResponse('prose, no capture')], { nudges: 3 })
|
||||
const run = ctx.subagents.start('spawn', structuredRequest(parent))
|
||||
const child = ctx.agents.get(run.id)!
|
||||
// Cancel synchronously inside the first turn's end recording — after the
|
||||
// turn reads `completed`, before the nudge continuation resumes. The turn
|
||||
// state alone cannot see this cancel (`child.cancel()` only clears
|
||||
// queued/running work), so without the loop's own cancelled check the
|
||||
// next send would spend a fresh child turn after the caller cancelled.
|
||||
ctx.on('session/event', (session, event) => {
|
||||
if (session === child.session && event.type === 'turn/end') run.cancel('cancelled between turn end and nudge')
|
||||
})
|
||||
const result = await run.result
|
||||
expect(result.stopReason).toBe('aborted')
|
||||
// Exactly one model request: the nudge turn never ran.
|
||||
expect(adapter.requests.length).toBe(1)
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('rejects a schema outside the subset loud, before any child exists', async () => {
|
||||
const { ctx, parent } = await setup([])
|
||||
expect(() => ctx.subagents.start('spawn', structuredRequest(parent, {
|
||||
|
||||
Reference in New Issue
Block a user