fix(subagent): contain a structuredClone failure on the detached subagent/end path
Review noted the deep-clone of the child output runs inside `onFulfilled`, OUTSIDE emitLifecycle's per-listener containment, and the settle `.then` is `void`ed — so an uncloneable output (a future non-serializable content-block type, or a contract-violating result) would throw and become an UNHANDLED rejection, contradicting the "any throw is contained" guarantee the comment claims. Wrap the clone in try/catch: on failure, log via ctx.logger.warn and emit subagent/end WITHOUT lastAssistantMessage (preserving stopReason/agentType) rather than dropping the event or crashing. Regression proves the unfixed code produces an unhandled rejection.
This commit is contained in:
@@ -207,8 +207,20 @@ export class SubagentService extends Service {
|
||||
// the SAME array reference the caller consumes would let a mutating
|
||||
// `subagent/end` listener corrupt the caller's SubagentResult.output —
|
||||
// breaking the observe-only contract. A snapshot makes the event a
|
||||
// read-only view, not a shared handle.
|
||||
this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: result.stopReason, lastAssistantMessage: structuredClone(result.output) })
|
||||
// read-only view, not a shared handle. The clone is wrapped: it runs
|
||||
// inside `onFulfilled`, OUTSIDE emitLifecycle's per-listener containment,
|
||||
// so an uncloneable value (a future non-serializable content-block type,
|
||||
// or a contract-violating result with no `output`) would otherwise become
|
||||
// an unhandled rejection on this detached `.then`. On clone failure, log
|
||||
// and emit the event WITHOUT lastAssistantMessage rather than dropping the
|
||||
// whole `subagent/end`.
|
||||
let lastAssistantMessage: SubagentResult['output'] | undefined
|
||||
try {
|
||||
lastAssistantMessage = structuredClone(result.output)
|
||||
} catch (error: unknown) {
|
||||
this.ctx.logger.warn(`subagent: could not clone ${name} output for subagent/end: ${String(error)}`)
|
||||
}
|
||||
this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: result.stopReason, ...lastAssistantMessage !== undefined ? { lastAssistantMessage } : {} })
|
||||
},
|
||||
() => { this.emitLifecycle('subagent/end', { provider: name, id: run.id, ...agentType, stopReason: 'error' }) },
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user