Agent Runner Timeout Enforcement#
The runner enforces five independent, layered time-based limits on every agent run to prevent wedged runs from holding sandboxes, mounts, and sockets indefinitely β and to avoid prematurely killing legitimate long-lived connections during human-in-the-loop (HITL) approval workflows.
All limits are implemented in run-limits.ts and wired into sandbox_agent.ts. The long-poll ACP transport layer is handled separately in acp-fetch.ts.
Timeout Layers#
Five distinct limits cover different failure modes :
| Limit | Env Variable | Default | Failure mode covered |
|---|---|---|---|
| Total deadline | AGENTA_RUNNER_RUN_TOTAL_TIMEOUT_MS | 45 min | Run never finishes at all |
| Idle / no-progress | AGENTA_RUNNER_RUN_IDLE_TIMEOUT_MS | 5 min | Run stalls mid-execution with no output |
| TTFB (Time To First Byte) | AGENTA_RUNNER_RUN_TTFB_TIMEOUT_MS | 2 min | Provider or adapter never starts responding |
| Per-tool-call | AGENTA_RUNNER_TOOL_CALL_TIMEOUT_MS | 5 min | Individual tool call hangs |
| ACP long-poll backstop | SANDBOX_AGENT_ACP_HEADERS_TIMEOUT_MS / SANDBOX_AGENT_ACP_BODY_TIMEOUT_MS | 60 min (or β prior to PR #5148) | HTTP transport reaped while awaiting HITL approval |
Idle clamping safety: If
AGENTA_RUNNER_RUN_IDLE_TIMEOUT_MSis configured at or above the total deadline, the runner automatically clamps idle to half the total and logs a warning β a misconfigured environment never breaks every run in the process.
How Limits Are Enforced#
All four run-scoped limits (total, idle, TTFB, per-tool-call) are managed by the RunLimitsHandle returned by createRunLimits(). When any limit trips, it fires a deadlineAbort controller that is merged with the caller's existing signal via AbortSignal.any() β the run aborts exactly as a client disconnect would, reusing the existing finally cleanup path with no new teardown logic.
Key wiring in sandbox_agent.ts:
- Progress tracking β The
emitcallback is wrapped withrunLimits.wrapEmit(emit)so every harness event (message deltas, tool results, usage, etc.) resets the idle and TTFB timers. - Per-tool-call tracking β
noteToolCallStart(id)starts the timer ontool_callACP updates;noteToolCallEnd(id)clears it oncompleted/failedstatus. - Pause exemption β When the pause signal resolves (a turn parks for HITL input),
runLimits.notePaused()freezes all timers permanently. The pause controller owns the turn's end from that point β run-limit deadlines must never reap a turn waiting on human input. - Cleanup β
runLimits.dispose()is called unconditionally in thefinallyblock, marking the instance inert so no late ACP event can re-arm a timer after teardown.
ACP Long-Poll Timeout (HITL Keep-Alive)#
A separate problem existed before PR #4848: undici's default headersTimeout (~5 min) was killing parked ACP connections while a human deliberated on a tool approval, causing UND_ERR_HEADERS_TIMEOUT to abort the paused turn.
acp-fetch.ts solves this by routing all ACP HTTP through a custom undici Agent (createAcpDispatcher) scoped exclusively to ACP traffic β other HTTP clients retain their safe defaults.
Original defaults in PR #4848: both headersTimeout and bodyTimeout set to 0 (disabled). In PR #5148, these were changed to 60 minutes to provide an explicit backstop without interfering with legitimate HITL waits β intentionally wider than the 45-minute total deadline so they never fire first during a normal run.
keepAliveTimeout and keepAliveMaxTimeout are both set to 600,000 ms (10 min) to prevent connection pooling from closing the socket mid-park.
HITL Approval Parking#
When a tool requires human approval and no stored decision exists, the runner returns a park outcome (introduced in PR #4848) instead of deny, which skips sending a respondPermission reply to the harness entirely. This prevents the previous bug where the deny reply's "User refused permission" error clobbered the approval prompt on the same tool_call_id. The turn ends with the tool pending; the next turn's stored decision resolves it.
This is runner-internal only β no wire protocol change to /run.
Key Source Files#
| File | Purpose |
|---|---|
sandbox_agent/run-limits.ts | resolveRunLimits(), createRunLimits(), all timeout constants and env vars |
engines/sandbox_agent.ts | Wiring: deadlineAbort, AbortSignal.any, wrapEmit, notePaused, dispose |
sandbox_agent/acp-fetch.ts | createAcpDispatcher() β long-timeout undici agent for HITL ACP connections |
runner/tests/unit/run-limits.test.ts | 283-line unit test suite for all timeout behaviors |