fix(desktop,host-service): create terminal sessions on WS attach so new panes survive HTTP pool starvation
Summary
- Under load, creating a new terminal pane in v2 silently did nothing until the window was refreshed. Every "new terminal" action awaited a
terminal.createSessionHTTP mutation before inserting the pane, with no timeout at any layer — and under load Chromium's 6-connections-per-origin pool to host-service gets pinned by long-running requests, so the mutation never reached the wire. - This PR inverts the flow for plain terminal creation: insert the pane synchronously and create the session during the pane's WebSocket attach (
create=1). WebSockets use a separate connection pool, so creation keeps working while HTTP is fully starved. - Paths that create sessions for panes that may never mount (background preset tabs, v1→v2 migration) keep the HTTP mutation, now bounded by
AbortSignal.timeout(30s)so they fail visibly instead of hanging forever.
Why / Context
Reproduced deterministically via CDP against the dev app: pin all 6 HTTP sockets to host-service with slow real requests (stand-in for what sidebar polls + minutes-long workspaces.create + 15–60s git/filesystem queries do under real load), then click + → Terminal:
- Before: the
createSessionPOST — and even its CORS preflight — sat unanswered for 47s. No tab, no spinner, no toast. Releasing the pins (what a refresh does to in-flight fetches) made it complete instantly, which is exactly the reported "can't create terminals until refresh". - After: same wedge, same click — pane visible in 15ms, shell attached in 401ms over the
create=1WebSocket, HTTP probe confirmed still blocked before and after.
The no-timeout stack that made this silent: raw tRPC client (QueryClient retry config doesn't apply to .mutate()), no fetch timeout on httpBatchStreamLink, and host-service's timeout middleware exempts mutations by design.
How It Works
- Renderer:
TerminalLaunchergainsmint()— returns a fresh terminalId, no network. All plain-create surfaces (tab-bar+, empty state, split hotkeys/button/context menu, session dropdown) insert the pane immediately withcreateOnAttach: trueinTerminalPaneData.TerminalPaneappendscreate=1to the terminal WS URL for flagged panes. - Host-service: the terminal WS attach handler's "no session row" branch calls the existing idempotent
createTerminalSessionInternalwhencreate=1+workspaceIdare present, deduped through the same in-flight map as respawns. Exited/disposed rows keep answeringsession-gone, so a persisted flag can never resurrect a closed shell. - Failure UX: creation failures now surface as a pane in a disconnected state with the existing diagnosis popover + retry, instead of a click that does nothing.
Also includes two one-line fixes (separate commit): the agent-resume work pulled terminal-agents/persistence.ts into terminal.ts's import chain with an extensionless value import and a TS parameter property, which silently broke every terminal *.node-test.ts under node --experimental-strip-types.
Manual QA Checklist
All verified via CDP against the dev desktop app with real trusted input (screenshots + WS/network event logs captured):
- Baseline: + → Terminal creates and attaches normally with no load (~500ms, unchanged)
- Wedge: with all 6 HTTP sockets pinned (probe blocked before and after), + → Terminal shows the pane in 15ms and attaches in 401ms
- Session dropdown "New Terminal" under the same wedge: attached in 402ms
- Rapid-create spam (3 quick creates): 3 distinct sessions, no duplicates
- Real-keystroke
exit, then reload: flagged pane re-attaches and replays the exit — dead shell shown, no respawn, live session count stable - Reload + mount every tab: every
create=1dial resolved to its existing id (attach, never a duplicate); pre-fix unflagged panes still dialcreate=0with the adopt/respawn path intact -
createOnAttachpersists in the pane layout and is honored safely (only fires when no session row exists at all)
Testing
bun run typecheckbun run lintcd packages/host-service && bun test— 1065 pass, 0 failnode --experimental-strip-types --test src/terminal/terminal.create-on-attach.node-test.ts— 7 tests against the real WS route + in-process pty-daemon: creates for brand-new ids, session-gone preserved for no-flag/exited-row/disposed-row, unknown workspaceId refused, concurrent attaches deduped to one session. Mutation-checked: disabling the create branch fails the suite.
Design Decisions
- Create-on-attach instead of a client timeout + toast: a timeout converts "silent forever-hang" into "error toast under load" — but creation still fails exactly when users need a terminal to investigate the load. The WS path verifiably keeps working while the HTTP pool is starved (fresh WS handshake: 94ms during full starvation), and it removes the create-before-insert ordering constraint rather than masking it.
createOnAttachdeliberately persists (not cleared after first attach): the host only honors it when no session row exists, so a stale flag can't clobber a live or exited session — and if a row is ever hard-deleted while the pane layout survives, the pane respawns a fresh shell instead of dead-ending.- Presets/migration keep HTTP: background preset tabs intentionally never mount (no WS to carry the create), and keeping commands off the WS URL avoids initial-command re-run semantics on reattach.
Known Limitations
- Under a pinned pool, preset execution into background tabs is still HTTP-bound — it now fails visibly after 30s instead of hanging, but doesn't bypass the pool.
- Relay-fronted hosts verified by inspection only (WS dial preceded by a 3s-bounded
_whoownspreflight to the relay origin over HTTP/2, which has no 6-connection limit) — no relay workspace on the dev stack to run the wedge harness against.
Follow-ups
- Batch the unbatched
httpLinkinrenderer/lib/host-service-client.ts(sidebar polls are the biggest steady-state consumer of the 6 sockets). - Keep minutes-long
workspaces.createmutations from squatting pooled sockets. plans/pty-lifecycle-cleanup.mditem 9: park = disconnect, jitter + cap on resume-reconnect fan-out (the separate WS handshake-throttle contributor).- One-line
finallyhardening for the migration hook'srunningRef(a throw mid-loop can wedge it for the session; pre-existing, now reachable via the 30s timeout).
Summary by cubic
Create terminal sessions on WebSocket attach so new panes appear instantly and keep working when the HTTP connection pool is saturated. Adds a 30s timeout to remaining HTTP-based creates to avoid silent hangs.
- Bug Fixes
- Renderer: insert panes immediately using
launcher.mint()+createOnAttach; updated all create surfaces (tab “+”, splits, dropdown, hotkeys). The pane’s WS attach appendscreate=1. Failures show a disconnected pane with retry. host-service: WS attach creates the session when no row exists andcreate=1+workspaceIdare provided; exited/disposed rows still returnsession-gone. Concurrent attaches are de-duped.themeTypehonored on attach. Added node tests for create-on-attach.- Presets/migration that may never mount still use HTTP, now with
AbortSignal.timeout(30_000). - Node strip-types compatibility: avoid TS parameter properties and use
.tsimport extensions on theterminal.tschain sonode --experimental-strip-typestests run.
- Renderer: insert panes immediately using
Summary by CodeRabbit
-
New Features
- Terminal panes and tabs now open more responsively by preparing sessions during attachment.
- Added support for creating terminal sessions directly when connecting, including workspace and theme handling.
- New terminal actions and split-pane shortcuts now respond immediately.
-
Bug Fixes
- Improved concurrent terminal creation to prevent duplicate sessions.
- Added a timeout for terminal session creation requests.
- Improved handling of missing, exited, or unavailable terminal sessions.