Summary
When a sub-agent run is force-killed by its run timeout (runTimeoutSeconds / subagents.runTimeoutSeconds), the parent session receives no completion/timeout event. The sub-agent registry correctly records status: "timeout" with an endedAt, but the parent agent is never notified — it waits indefinitely and only discovers the death by actively polling subagents(action=list).
Normal completions (and graceful exits that still produce output) notify the parent reliably. The gap is specific to the timeout/force-kill termination path.
Version: 2026.5.28 (e932160).
Reproduction
- From a parent agent, spawn a sub-agent with a short run timeout and a blocking task, e.g.
runTimeoutSeconds: 30 and a task that runs sleep 600.
- The runtime force-kills the child at 30s;
subagents(action=list) shows status: "timeout", endedAt set.
- The parent session receives no completion event — no auto-announce message arrives. Contrast with a normally-finishing sub-agent, whose completion arrives as a parent message within seconds.
Root cause
In the wait-timer fallback inside waitForAgentJob (dist/agent-D5XvMyya.js, ~line 339), the timeout callback only consults getPendingAgentRunError and ignores getPendingAgentRunTimeout:
const timer = setSafeTimeout(() => {
const pendingError = getPendingAgentRunError(runId);
finish(pendingError ? createPendingErrorTimeoutSnapshot(pendingError.snapshot) : null);
}, timeoutMs);
When the run timeout fires, the gateway force-kills the child and a pending timeout snapshot (carrying endedAt) is registered. But the wait timer fires near-simultaneously and calls finish(null) because there is no pending error — discarding the timeout snapshot and cancelling the grace timer. The resolved wait result then lacks endedAt/stopReason, so isTerminalWaitTimeout is false downstream and the announce/notify path never delivers a parent event.
The normal scheduled timeout path already resolves pendingTimeout.snapshot directly, so the wait-timer fallback is simply asymmetric — it handles pending errors but not pending timeouts.
Suggested fix
Make the wait-timer fallback symmetric — fall back to the pending timeout snapshot when there is no pending error:
const timer = setSafeTimeout(() => {
const pendingError = getPendingAgentRunError(runId);
if (pendingError) {
finish(createPendingErrorTimeoutSnapshot(pendingError.snapshot));
return;
}
const pendingTimeout = getPendingAgentRunTimeout(runId);
finish(pendingTimeout ? pendingTimeout.snapshot : null);
}, timeoutMs);
getPendingAgentRunTimeout(runId) already exists with a signature matching getPendingAgentRunError (both return { snapshot, dueAt } or undefined). This preserves error precedence and keeps the timeout snapshot's endedAt, so isTerminalWaitTimeout becomes true and the parent receives the timeout completion.
Possibly related (not fixed here)
There is a secondary "no-output" case in the announce delivery layer (subagent-announce-delivery-*.js, hasFailedSubagentNoOutputCompletion): a timed-out sub-agent that produced literally (no output) is marked delivered:false and dropped after retry. This is plausibly intentional (don't push an empty message), but worth confirming it doesn't compound the silent-timeout symptom for the parent-session event specifically.
This was found via adversarial testing of the sub-agent comm path; the wait-timer fix above resolved the core "silent timeout" symptom locally. Filing for an upstream fix.
Summary
When a sub-agent run is force-killed by its run timeout (
runTimeoutSeconds/subagents.runTimeoutSeconds), the parent session receives no completion/timeout event. The sub-agent registry correctly recordsstatus: "timeout"with anendedAt, but the parent agent is never notified — it waits indefinitely and only discovers the death by actively pollingsubagents(action=list).Normal completions (and graceful exits that still produce output) notify the parent reliably. The gap is specific to the timeout/force-kill termination path.
Version:
2026.5.28 (e932160).Reproduction
runTimeoutSeconds: 30and a task that runssleep 600.subagents(action=list)showsstatus: "timeout",endedAtset.Root cause
In the wait-timer fallback inside
waitForAgentJob(dist/agent-D5XvMyya.js, ~line 339), the timeout callback only consultsgetPendingAgentRunErrorand ignoresgetPendingAgentRunTimeout:When the run timeout fires, the gateway force-kills the child and a pending timeout snapshot (carrying
endedAt) is registered. But the wait timer fires near-simultaneously and callsfinish(null)because there is no pending error — discarding the timeout snapshot and cancelling the grace timer. The resolved wait result then lacksendedAt/stopReason, soisTerminalWaitTimeoutisfalsedownstream and the announce/notify path never delivers a parent event.The normal scheduled timeout path already resolves
pendingTimeout.snapshotdirectly, so the wait-timer fallback is simply asymmetric — it handles pending errors but not pending timeouts.Suggested fix
Make the wait-timer fallback symmetric — fall back to the pending timeout snapshot when there is no pending error:
getPendingAgentRunTimeout(runId)already exists with a signature matchinggetPendingAgentRunError(both return{ snapshot, dueAt }orundefined). This preserves error precedence and keeps the timeout snapshot'sendedAt, soisTerminalWaitTimeoutbecomes true and the parent receives the timeout completion.Possibly related (not fixed here)
There is a secondary "no-output" case in the announce delivery layer (
subagent-announce-delivery-*.js,hasFailedSubagentNoOutputCompletion): a timed-out sub-agent that produced literally(no output)is markeddelivered:falseand dropped after retry. This is plausibly intentional (don't push an empty message), but worth confirming it doesn't compound the silent-timeout symptom for the parent-session event specifically.This was found via adversarial testing of the sub-agent comm path; the wait-timer fix above resolved the core "silent timeout" symptom locally. Filing for an upstream fix.