fix(shell): key PTY sessions by client identity and pre-assign Claude session ids - #1005
fix(shell): key PTY sessions by client identity and pre-assign Claude session ids#1005mayankdebnath wants to merge 1 commit into
Conversation
… session ids
The PTY registry keyed shells as `${projectPath}_${sessionId ?? 'default'}`,
which conflated distinct logical shells and lost track of a session once its
id materialized:
- Every new-session shell in a project collapsed to one `_default` key, so a
second tab silently attached to the first tab's process ("[Reconnected to
existing session]") — both terminals drove the same PTY while only the
newest rendered output.
- When the same conversation was later opened by its session id, the key no
longer matched, so the server spawned a duplicate `claude --resume` while
the original process kept running (and consuming tokens) under `_default`
until the 30-minute reaper fired.
Fix:
- The client sends a stable per-tab `shellClientId` (sessionStorage-persisted,
so tabs are distinct but a remount in the same tab still reattaches). New-
session keys include it; session-keyed shells ignore it so cross-device
handoff is unchanged. Clients that don't send the field keep the legacy key.
- Brand-new Claude sessions launch with a pre-assigned `--session-id <uuid>`
and register a sessionId -> ptyKey alias, so a later by-id open reattaches
to the same PTY instead of forking a duplicate resume.
- Attaching to a PTY whose socket is still open notifies and detaches the old
client explicitly (banner + `session_detached` message) instead of stealing
it silently, a detached socket can no longer write into or resize the shared
PTY, and the superseded client closes its socket on `session_detached` so its
UI drops into the normal disconnected state instead of eating keystrokes.
- Restarting a shell (forceRestart/login) recomputes the registry key after the
kill, so a restarted conversation is re-registered under its canonical key
rather than stranded under the previous tab's identity key.
- `--session-id` launches carry a plain-`claude` fallback (mirroring the resume
fallback) so CLIs that predate the flag still start.
- A stale socket close no longer detaches the current client or arms a kill
timer against a PTY a newer socket owns (the detach half of siteboon#953), and the
reaper timer is cleared before being rearmed.
Verified end-to-end against the built server with two concurrent websocket
clients: distinct tabs now get separate PTYs, legacy clients keep the reattach
behavior, the old client receives the detach notice, and detached input is
blocked. Also verified with Claude Code v2.1.206 that `--session-id <uuid>`
creates exactly that session and `--resume <uuid>` continues the same file.
Fixes siteboon#1004
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe PR adds stable per-tab shell identities, deterministic PTY routing, Claude session aliases, explicit websocket detachment, and ownership-aware cleanup. It also extends shell message types and adds tests for routing and command construction. ChangesShell session identity
Sequence Diagram(s)sequenceDiagram
participant ShellHook
participant ShellWebSocketService
participant ClaudePTY
ShellHook->>ShellWebSocketService: init with shellClientId or sessionId
ShellWebSocketService->>ShellWebSocketService: resolve PTY key or session alias
ShellWebSocketService->>ClaudePTY: launch or reattach Claude PTY
ClaudePTY-->>ShellWebSocketService: assigned session output
ShellWebSocketService-->>ShellHook: output or session_detached
Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Provider-scope note, for reviewers weighing how much of #1004 this covers per provider:
|
Summary
Fixes #1004: the
/shellPTY registry keyed sessions as${projectPath}_${sessionId ?? 'default'}, which conflated distinct logicalshells and lost track of a conversation once its session id materialized. The
result was silent cross-tab hijack ("you think you're looking at your shell but
you're attached to a different process") and duplicate
claude --resumeprocesses with the original left running — burning tokens on output nobody sees
— until the 30-minute reaper.
The two failure modes (details in #1004)
_defaultkey, so a second tab silently attached to the first tab'sprocess. Both terminals then drove the same PTY while only the newest
rendered output.
_default; opening the same conversation by its session id later produced adifferent key, so the server spawned a second
claude --resume <id>whilethe original process kept running under
_default.The fix
shellClientId(persisted in
sessionStorage, so two tabs are distinct but a remount in thesame tab reuses it). New-session keys include it — distinct tabs now get
distinct PTYs; the same tab still reattaches. Session-keyed shells ignore it,
so cross-device handoff to a resumed session is unchanged. Clients that don't
send the field keep today's exact behavior.
claude --session-id <server-generated uuid>and register asessionId → ptyKeyalias. A later init that opens the conversation by idresolves through the alias and reattaches to the same PTY instead of
forking a duplicate resume.
notifies the old client (yellow banner + structured
session_detachedmessage) before rebinding, instead of stealing it silently — and a detached
socket can no longer write into or resize the shared PTY.
closefrom a superseded socket no longerdetaches the live client or arms a 30-minute kill timer against the PTY a
newer socket owns (this is the "stale close detaches the live PTY" half of
Shell terminal silently freezes on mobile while Chat keeps updating — stale WebSocket close detaches the live PTY socket; half-open sockets never reaped #953; the half-open socket reaping from Shell terminal silently freezes on mobile while Chat keeps updating — stale WebSocket close detaches the live PTY socket; half-open sockets never reaped #953 is intentionally out of scope
here). The reaper timer is also cleared before being rearmed.
Key resolution and alias routing are extracted as pure exported functions
(
resolvePtySessionKey,resolveSessionAlias) with unit tests.Verification
clients (register → two
/shellsockets → init):shellClientIds → separate PTYs (no"[Reconnected to existing session]" hijack) ✅
session_detachedmessage ✅claude --session-id <uuid> -p …creates exactly<uuid>.jsonl, and asubsequent
claude --resume <uuid>continues the same session file — sopre-assignment + later reattach cannot fork the conversation.
node:test) for key resolution, alias routing, and--session-idcommand building across platforms and providers; full suite,npm run lint,npm run typecheck, andnpm run buildall pass.Hardening found during self-review
While stress-testing the edge cases before opening this, I found and fixed
three interactions in the initial version of this change:
the aliased PTY but respawned it under the previous tab's identity key,
which would have re-created the duplicate/orphan problem after a restart.
The registry key is now recomputed after the kill so the restarted PTY
lands under its canonical key.
--session-idwas added to the Claude CLI in mid-2025; olderbinaries exit with
error: unknown option. New-session launches now carrythe same
|| claude/$LASTEXITCODEfallback the resume path alreadyuses. (On such CLIs the pre-assigned id never exists, so its alias is
unreachable and simply ages out.)
session_detachedbyclosing its socket, so the losing window drops into the normal disconnected
state (with the reconnect affordance) instead of looking attached while the
server discards its input.
Known limitations (unchanged from today's behavior)
window.openclonessessionStorage, soit shares its source tab's identity and degrades to the current shared-PTY
behavior — now with an explicit detach notice rather than a silent steal.
shell is still alive reattaches to that shell (same as today's
_defaultbehavior); distinct tabs are the case this PR separates.
Compatibility notes
initgains an optionalshellClientIdand the server mayemit
session_detached; both are ignored by older peers, so mixed versionsdegrade to today's behavior.
I'll rebase whichever lands second).
Summary by CodeRabbit
New Features
Bug Fixes