fix(subagent): address codex review round 1
All five findings were real: - The terminal lifecycle edge derived its stop reason from teardown success, so a child that errored, hit its token ceiling, or was cancelled reported as completed once its checkpoint and disposal succeeded. It now reads the child's own last message turn/end, which is authoritative. - Live delivery never rechecked the caller signal after authorization yielded, so an abort that won before acceptance still enqueued the message and returned an id. Admission now re-checks at the boundary that owns the decision. - Drain flushed before cancelling, letting a running turn keep appending events the checkpoint could not cover and letting model work continue through a slow flush. It now cancels to quiescence first. - subagent/end fired after AgentHandle.dispose() unregistered the child, so the hooks bridge could not resolve it for the child's cwd and scope. The edge now publishes while the child is still registered. - activationState() read Agent.status alone, which stays idle between an accepted waking send and the microtask that admits it, so a synchronous inbox observer could see settled with a queued turn. Residency now also counts messages this manager admitted but has not seen leave the inbox.
This commit is contained in:
@@ -36,6 +36,7 @@ import type { Scoped } from '@deepseek-ai/dsh-scope'
|
||||
import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools'
|
||||
import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import { findLastMessageTurnEnd } from '@deepseek-ai/dsh-session'
|
||||
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import type {
|
||||
ContinuableCreateRequest,
|
||||
@@ -377,7 +378,7 @@ export class SubagentService extends Service {
|
||||
const output = failure === undefined ? lastAssistantOutput(child) : undefined
|
||||
this.emitLifecycle('subagent/end', {
|
||||
...identity,
|
||||
stopReason: failure === undefined ? 'completed' : 'error',
|
||||
stopReason: failure === undefined ? childStopReason(child) : 'error',
|
||||
...output === undefined ? {} : { lastAssistantMessage: output },
|
||||
}, parent)
|
||||
},
|
||||
@@ -459,6 +460,37 @@ export class SubagentService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Why this child's last ordinary turn ended, for the terminal lifecycle edge.
|
||||
* The child's own `turn/end` is authoritative: teardown succeeding says nothing
|
||||
* about whether the model errored, hit its token ceiling, or was cancelled, so
|
||||
* deriving the reason from disposal would report failed work as completed.
|
||||
* @param child - the settling child agent whose log is read.
|
||||
* @returns its terminal stop reason; `completed` when no ordinary turn closed.
|
||||
*/
|
||||
function childStopReason(child: Agent): SubagentResult['stopReason'] {
|
||||
const reason = findLastMessageTurnEnd(child.session.events)?.data.reason
|
||||
// No ordinary turn closed, so nothing failed either.
|
||||
if (reason === undefined) return 'completed'
|
||||
switch (reason.kind) {
|
||||
case 'max-tokens':
|
||||
return 'max-tokens'
|
||||
case 'aborted':
|
||||
case 'interrupted':
|
||||
case 'disposed':
|
||||
return 'aborted'
|
||||
case 'error':
|
||||
return 'error'
|
||||
case 'completed':
|
||||
return 'completed'
|
||||
/* v8 ignore next 3 -- `TurnEndReason` is merge-extensible, so this arm needs a
|
||||
* backend that adds a variant; treating an unnameable reason as success would
|
||||
* report failed work as completed. */
|
||||
default:
|
||||
return 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The child's last assistant message content, for one Activation's terminal
|
||||
* lifecycle edge. Absent when no assistant message reached the log.
|
||||
|
||||
Reference in New Issue
Block a user