Skip to content

fix(terminal): finalize sessions abandoned mid-handshake so slots aren't leaked - #579

Open
Farahat612 wants to merge 1 commit into
oblien:mainfrom
Farahat612:fix/426-terminal-session-leak
Open

fix(terminal): finalize sessions abandoned mid-handshake so slots aren't leaked#579
Farahat612 wants to merge 1 commit into
oblien:mainfrom
Farahat612:fix/426-terminal-session-leak

Conversation

@Farahat612

Copy link
Copy Markdown

A browser refresh/close during WS open lands in onClose while onOpen is
still awaiting the SSH channel and audit-row insert. teardown then ran
with no sessionId, marked the connection ended, and onOpen resumed to
register a session nobody owns: the idle/cap timeout's teardown became a
no-op, leaving the terminal_sessions row open forever. Since the
handshake cap counts DB rows with endedAt IS NULL, each abandoned open
permanently burned a per-user slot until a restart's closeAllActive()
sweep (#426).

teardown now defers to onOpen while no session is registered (leaving
state.closed set as the abort signal), and onOpen aborts after
registration when the WS is already gone: unregistering and finalizing
the row instead of wiring a dead connection. Park/resume after "ready"
is unchanged. Same-shape fix in the service-terminal controller.

New regression test drives the real handler bundle with onClose landing
mid-openShell: fails before (in-memory + DB counts stay 1), passes
after; a sibling test pins that post-ready disconnects still park.

Closes #426.

Root cause in one line: the WebSocket close listener is registered before
the async open handler resolves, so a browser refresh mid-handshake ran
teardown with no sessionId, marked the connection ended, and the session
registered moments later could never be finalized. The terminal_sessions row
(whose endedAt IS NULL count gates the 3-session cap) stayed open until a
restart's closeAllActive() sweep, which matches the reported "persists for
days, docker compose restart fixes it".

How verified: the new test drives the real handler bundle with onClose
landing while onOpen is suspended inside the SSH channel open. It fails on
main (in-memory and DB active counts stay 1) and passes with the fix. A
sibling test pins that a disconnect after the ready frame still parks the
session for resume, so the intentional park/resume design is unchanged. The
same-shape fix is applied to the service-terminal controller. Full apps/api
suite green (2993 tests), typecheck clean.

…n't leaked

A browser refresh/close during WS open lands in onClose while onOpen is
still awaiting the SSH channel and audit-row insert. teardown then ran
with no sessionId, marked the connection ended, and onOpen resumed to
register a session nobody owns: the idle/cap timeout's teardown became a
no-op, leaving the terminal_sessions row open forever. Since the
handshake cap counts DB rows with endedAt IS NULL, each abandoned open
permanently burned a per-user slot until a restart's closeAllActive()
sweep (oblien#426).

teardown now defers to onOpen while no session is registered (leaving
state.closed set as the abort signal), and onOpen aborts after
registration when the WS is already gone: unregistering and finalizing
the row instead of wiring a dead connection. Park/resume after "ready"
is unchanged. Same-shape fix in the service-terminal controller.

New regression test drives the real handler bundle with onClose landing
mid-openShell: fails before (in-memory + DB counts stay 1), passes
after; a sibling test pins that post-ready disconnects still park.
@Farahat612
Farahat612 force-pushed the fix/426-terminal-session-leak branch from d965339 to 313c87e Compare August 16, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Terminal session counter not reset after browser disconnect — ghost sessions block new shells indefinitely

1 participant