Characterization (from release QA of feat/sessions-storage-rework, 2026-07-28)
Three related behaviors when two clients drive the SAME session concurrently (local sandbox, single runner replica). These may be acceptable v0 trade-offs; filing so the design intent gets decided explicitly rather than by accident.
-
No start-time gating. Turn X in flight; turn Y arrives on the same session_id from a second connection. Both run fully in parallel, each in its OWN cold-started local sandbox pointed at the same durable mount path. There is no one-active-turn-per-session gate at start; exclusivity exists only after the fact via the heartbeat.
-
Loser reaped up to 30s late. X only learns it lost at its next heartbeat (HEARTBEAT_INTERVAL_SECONDS = 30): is_current_turn=false -> cooperative abort, open tool call settled as INTERRUPTED_BY_USER, wire finish with finishReason: "other". Until that beat it burns tokens and sandbox time on work that will be thrown away. Observed abort at exactly T+30.055s after turn start.
-
Disconnect does not cancel. Closing the losing client's SSE connection mid-stream does not abort its run (session-owned disconnect only flags clientGone). The turn keeps executing with no client watching until the same heartbeat-driven reap.
Follow-on bug filed separately: #5538 (the turn right after a takeover can land on the pool entry mid-teardown and fail with a user-visible internal error).
Good news verified at the same time: the durable record log stayed internally consistent through all of this (no duplicate record ids, strictly monotonic timestamps).
Relevant code: services/runner/src/sessions/alive.ts (heartbeat + interrupted), run-turn.ts abort path, server.ts disconnect handling.
https://claude.ai/code/session_01VhviVuZExGJUnY28mzbPBK
Characterization (from release QA of
feat/sessions-storage-rework, 2026-07-28)Three related behaviors when two clients drive the SAME session concurrently (local sandbox, single runner replica). These may be acceptable v0 trade-offs; filing so the design intent gets decided explicitly rather than by accident.
No start-time gating. Turn X in flight; turn Y arrives on the same session_id from a second connection. Both run fully in parallel, each in its OWN cold-started local sandbox pointed at the same durable mount path. There is no one-active-turn-per-session gate at start; exclusivity exists only after the fact via the heartbeat.
Loser reaped up to 30s late. X only learns it lost at its next heartbeat (
HEARTBEAT_INTERVAL_SECONDS= 30):is_current_turn=false-> cooperative abort, open tool call settled asINTERRUPTED_BY_USER, wirefinishwithfinishReason: "other". Until that beat it burns tokens and sandbox time on work that will be thrown away. Observed abort at exactly T+30.055s after turn start.Disconnect does not cancel. Closing the losing client's SSE connection mid-stream does not abort its run (session-owned disconnect only flags
clientGone). The turn keeps executing with no client watching until the same heartbeat-driven reap.Follow-on bug filed separately: #5538 (the turn right after a takeover can land on the pool entry mid-teardown and fail with a user-visible internal error).
Good news verified at the same time: the durable record log stayed internally consistent through all of this (no duplicate record ids, strictly monotonic timestamps).
Relevant code:
services/runner/src/sessions/alive.ts(heartbeat + interrupted),run-turn.tsabort path,server.tsdisconnect handling.https://claude.ai/code/session_01VhviVuZExGJUnY28mzbPBK