fix(hub): allow terminal re-registration after socket reconnect#434
fix(hub): allow terminal re-registration after socket reconnect#434hqhq1025 merged 3 commits intotiann:mainfrom
Conversation
When a web client reconnects (common in PWAs and after network hiccups), it retains the same terminal ID but gets a new socket ID. The previous code rejected the registration because the old entry still existed, producing "Terminal ID is already in use". Now the registry treats a different-socket registration for the same terminal ID as a stale entry and cleans it up before re-registering. Same-socket re-registration returns the existing entry (idempotent). Terminal IDs are client-generated UUIDs so cross-client collisions are not a realistic concern. Closes tiann#345
There was a problem hiding this comment.
Findings
- [Major] Reconnects can still fail once the session is already at the terminal limit. The new stale-entry replacement in
hub/src/socket/terminalRegistry.ts:27is never reached ifterminal:createrejects onhub/src/socket/handlers/terminal.ts:103, so the sameterminalIdcan still getToo many terminals open for this sessioninstead of reattaching.
Suggested fix:const existing = terminalRegistry.get(terminalId) const isReconnect = existing?.sessionId === sessionId if (!isReconnect && terminalRegistry.countForSession(sessionId) >= maxTerminalsPerSession) { emitTerminalError(terminalId, `Too many terminals open for this session (max ${maxTerminalsPerSession}).`) return } const entry = terminalRegistry.register(terminalId, sessionId, socket.id, cliSocketId)
Summary
- Review mode: initial
- 1 issue found: the reconnect fix is incomplete because the per-session quota check still runs before stale-entry cleanup.
Testing
- Not run (automation):
bunis not installed in this runner. - Not found in repo/docs: a regression test covering reconnect with the same
terminalIdon a new socket while the session is already at the terminal limit.
HAPI Bot
| // previous connection (e.g. socket reconnect in a PWA). | ||
| // Terminal IDs are client-generated UUIDs so cross-client | ||
| // collisions are not a realistic concern; clean up and re-register. | ||
| this.remove(terminalId) |
There was a problem hiding this comment.
[MAJOR] This replacement path still misses one reconnect case: terminal:create rejects on countForSession() before register() is called (hub/src/socket/handlers/terminal.ts:103-114). If the session is already at HAPI_TERMINAL_MAX_TERMINALS, the stale entry is still counted and the reconnect fails with the max-terminals error, so the new cleanup never runs.
Suggested fix:
const existing = terminalRegistry.get(terminalId)
const isReconnect = existing?.sessionId === sessionId
if (!isReconnect && terminalRegistry.countForSession(sessionId) >= maxTerminalsPerSession) {
emitTerminalError(terminalId, `Too many terminals open for this session (max ${maxTerminalsPerSession}).`)
return
}
const entry = terminalRegistry.register(terminalId, sessionId, socket.id, cliSocketId)When a stale terminal entry still occupies a slot, the per-session and per-socket quota checks reject the reconnecting client before register() can clean up the stale entry. Detect reconnects (same terminalId + sessionId already registered) and bypass quota checks so the stale entry is properly replaced in register().
There was a problem hiding this comment.
Findings
- [Major] Reused terminal IDs can evict an active terminal from another session.
hub/src/socket/terminalRegistry.ts:28now removes any existing entry for a different socket, but the reconnect guard inhub/src/socket/handlers/terminal.ts:98only treats same-session reuse as valid. BecauseterminalIdis still client-supplied (shared/src/socket.ts:6), a create for another session can silently steal the mapping instead of returning "already in use".
Summary
Review mode: follow-up after new commits
1 remaining issue found: cross-session terminal ID reuse now replaces the old registry entry instead of rejecting the conflict.
Testing
Not run (automation): review policy avoided executing PR code.
Not found in repo/docs: a regression test covering reuse of the same client-supplied terminalId from a different session.
HAPI Bot
| // previous connection (e.g. socket reconnect in a PWA). | ||
| // Terminal IDs are client-generated UUIDs so cross-client | ||
| // collisions are not a realistic concern; clean up and re-register. | ||
| this.remove(terminalId) |
There was a problem hiding this comment.
[MAJOR] Replacing any existing entry here is too broad. The new reconnect check in hub/src/socket/handlers/terminal.ts:98 only classifies same-session reuse as reconnect, but register() will still delete an entry from a different session. Since terminalId is client-supplied (shared/src/socket.ts:6), a conflicting create can silently evict the original terminal instead of returning "Terminal ID is already in use."
Suggested fix:
const existing = this.terminals.get(terminalId)
if (existing) {
if (existing.socketId === socketId) {
return existing
}
if (existing.sessionId !== sessionId) {
return null
}
this.remove(terminalId)
}Only allow stale-entry replacement when the existing entry belongs to the same session. If a different session happens to present the same terminal ID, reject it as before to prevent one session from evicting another session's active terminal.
There was a problem hiding this comment.
Findings
- No new diff-attributable issues found in the current head.
Summary
Review mode: follow-up after new commits
No remaining diff-attributable issues found after the latest fix. Residual risk: Not found in repo/docs: a regression test covering same-session terminal re-registration alongside the existing terminal handler cases in hub/src/socket/handlers/terminal.test.ts.
Testing
Not run (automation): review policy avoided executing PR code.
Not found in repo/docs: regression coverage for the reconnect / re-registration path.
HAPI Bot
Summary
Fix "Terminal ID is already in use" error that occurs when web clients reconnect to a terminal session, particularly in PWA environments.
Problem
When a web socket reconnects (common in PWAs after network hiccups or tab backgrounding), the client retains the same terminal ID but receives a new socket ID. The
TerminalRegistry.register()method rejected the registration because the old entry still existed in the registry — the disconnect cleanup hadn't completed or the socket never properly disconnected.This was reported on a Windows runner + Ubuntu hub setup with the PWA web app.
Fix
Make
register()handle re-registration gracefully:Terminal IDs are client-generated UUIDs (
crypto.randomUUID()), so cross-client collisions are not a realistic concern. A different socket presenting the same terminal ID is reliably a reconnection, not a conflict.Changes
Single file:
hub/src/socket/terminalRegistry.ts— only theregister()method.Closes #345