Session Pool Management#
The runner's keep-alive pool holds harness sessions alive between turns so that subsequent turns can continue the live process instead of cold-replaying the entire conversation. This is implemented in session-pool.ts, introduced in PR #5156 and extended in PR #5615 .
The pool is disabled by default and scoped per-project: pool keys are {project_id}:{session_id}. Enable it with AGENTA_RUNNER_SESSION_KEEPALIVE=true .
History Fingerprinting for Session Matching#
Before resuming a parked session, the runner checks three fingerprints :
| Fingerprint | What it covers |
|---|---|
| Config fingerprint | Hash of harness, sandbox, model, provider, tools, permissions, and system prompt β excludes per-turn volatiles |
| History fingerprint | Hash of ordered user message texts and tool-call IDs (assistant text excluded); a message edit or rewind will trip a mismatch |
| Credential epoch | Process-local SHA256 over {secrets, toolCallbackAuth} plus mount credential expiry |
All three must match and the incoming turn's tail must be a plain user message. Any mismatch degrades to cold replay β no turn is ever failed .
PR #5615 added attachment IDs and inline-media digests to the history fingerprint, and modelCapabilities to the config fingerprint .
Pool Configuration#
| Variable | Default | Effect |
|---|---|---|
AGENTA_RUNNER_SESSION_KEEPALIVE | false | Enable the pool |
AGENTA_RUNNER_SESSION_POOL_MAX | 8 | LRU cap |
AGENTA_RUNNER_SESSION_TTL_MS | 60 000 ms | Idle TTL after a clean turn |
AGENTA_RUNNER_SESSION_APPROVAL_TTL_MS | 600 000 ms | Extended TTL for sessions parked on HITL approval |
Busy sessions and sessions awaiting approval are never LRU-evicted .
Session Resumption and Eviction#
When a turn ends cleanly the session parks in the pool. The next inbound request for the same (project_id, session_id) key looks up the parked entry, recomputes fingerprints, and either:
- Resumes β sends only the new user text via
session/load-style continuation (~1.4 s on Daytona vs ~12.5 s cold) , or - Evicts β tears down the parked session and cold-replays from scratch.
The pool evicts on any of: config mismatch, history mismatch, credential mismatch, busy state, LRU overflow, or TTL expiry .
Mismatch Handling on Non-First Turns#
On multi-turn sessions, the history fingerprint covers all prior turns. Accumulated tool-call IDs and user messages create more surface area for mismatch with each turn. Two failure modes documented in issue #5593 and related threads:
- Tool message synthesis divergence β the playground reconstructs approved tool calls as an assistant + synthesized
role: "tool"message pair. If the runner stored them in a different form, the hash breaks. - Message filtering on reconstruction β
constructChatHistoryruns validity checks and.filter(Boolean)before sending; filtered messages cause the fingerprint to diverge.
PR #5615 addresses this with a historyAsserted boolean on the ParkInput and LiveSession interfaces. A park from a minimal-history client (e.g., one that sends only the current user turn) sets historyAsserted: false; on resume, the runner then compares the fingerprint against only historyTailFromLastUserTurn(incomingPrior) rather than the full transcript . For out-of-band approval replies that assert no conversation at all, history comparison is skipped entirely. historyAsserted defaults to true (strictest check) when omitted.
Session Eviction on Unmatched Approval Answers (Bug)#
When an approval is answered out-of-band β from an inbox, webhook, or script β the runner cannot match it to a parked gate. Under the current policy (pre-fix), any unmatched approval answer evicts the live session, logging:
[keepalive] approval-mismatch ... ; evict + cold
This forces a cold rebuild even though the session was still within its 5-minute TTL . The correct behavior (per issue #5596) is to return an error and leave the session parked .
A compounding issue: the runner emits two IDs for a parked gate β the permission gate ID (token) and the tool call ID. Only token is persisted in durable storage; the tool_call_id is only available on the live event stream. Out-of-band answers built from the stored row alone identify the wrong call :
token = 3f71c92c-c5c0-489d-9739-1aa41069d6d3
resolution = {"verdict": "approved", "tool_call_id": "toolu_01SJ2SuKPMZfCMJbXj9hZp8c"}
Message History Reconstruction After Tool Approvals#
On the runner side, responder.ts scopes which tool_result blocks satisfy a pending client-tool call to the current turn only: currentTurnToolResultBlocks yields only blocks at or after the latest user-role message boundary . This prevents turn-2's request_input from silently reusing turn-1's answer.
On the frontend side, the resume HTTP body is assembled by the AI SDK from the current message array and includes tool-call parts, data-render sibling parts (render hints), and tool_result blocks injected by addToolOutput . buildRenderMap(parts) reconstructs the render hint map at resume time for consistent tool classification.
Key Files#
| File | Purpose |
|---|---|
session-pool.ts | Keep-alive pool; fingerprinting; TTL/LRU eviction |
session-continuity.ts | In-memory (sessionId, harness) β {agentSessionId, turnIndex} store |
session-continuity-durable.ts | Durable mirror to session_states.data; survives runner restart |
sandbox-reconnect.ts | Reconnect ladder: stored ID β restart β cold create |
responder.ts | currentTurnToolResultBlocks; cross-turn scoping |
sandbox_agent.ts | Top-level lifecycle (acquireEnvironment / runTurn) |
See Also#
- Sandbox Infrastructure β broader sandbox lifecycle (states, Daytona warm pool, durable mounts)
- Human-in-the-Loop Tool Approval β resume predicate, queue gating, approval metadata guard
- Tool Result Handling and Resume Flow β settlement chain, message array composition