fix: address codex review round 3

- Strict steer additionally requires an OPEN STEP: between steps the
  loop may be awaiting its continuation/turn-stop checkpoints, where
  pending steering was already folded and a terminal stop discards a
  later arrival. A message accepted during an open step is drained and
  recorded at that step's settlement before any terminal decision, so
  the acknowledged-then-discarded window is closed. New keyless test
  holds agent/turn-stop open and pins the rejection.
- tool-subagent-control README: distinguish synchronous not-delivered
  errors from started-Task failures (unknown/foreign/descriptor-less
  ids settle the started Task as failed), and drop the claim that the
  completion notice carries the child's response.
This commit is contained in:
Dudu-0223
2026-07-23 18:47:19 +08:00
committed by imccyu
parent 4eda48d002
commit 9e5ae0d12e
7 changed files with 49 additions and 7 deletions

View File

@@ -258,6 +258,35 @@ describe('startInProcessRun', () => {
await run.dispose()
})
it('strict steer rejects the between-steps window where a terminal turn-stop discards steering', async () => {
// Hold `agent/turn-stop` open: the step has closed, pending steering was
// already folded into the continuation decision, and a terminal stop
// would discard a message arriving now — the exact window an
// acknowledged delivery would be a lie.
const { ctx, parent } = await setup([textResponse('quick')])
let releaseStop: (() => void) | undefined
ctx.on('agent/turn-stop', (agent) => {
if (agent.session.header.parentSession === undefined || releaseStop !== undefined) return undefined
return new Promise((resolve) => {
releaseStop = () => { resolve(undefined) }
})
})
const run = await startInProcessRun(request(parent), {})
const child = ctx.agents.get(run.id)!
await new Promise<void>((resolve) => {
const timer = setInterval(() => {
if (releaseStop !== undefined) { clearInterval(timer); resolve() }
}, 5)
})
expect(child.status).toBe('running')
expect(() => { run.steer!([{ type: 'text', text: 'too late for this turn' }]) })
.toThrow(/between steps; the message was not delivered/)
releaseStop!()
await run.result
expect(child.session.events.some(event => event.type === 'steering/message')).toBe(false)
await run.dispose()
})
it('strict steer rejects the closed-turn flush window where the loop discards steering', async () => {
// Hold the turn-end durability flush open: the turn has closed in the log
// and status is still `running`, exactly the window where the loop would