fix(desktop,host-service): create terminal sessions on WS attach so new panes survive HTTP pool starvation

Merged
KI
3mo ago
+433-76

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.createSession HTTP 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 createSession POST — 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=1 WebSocket, 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: TerminalLauncher gains mint() — 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 with createOnAttach: true in TerminalPaneData. TerminalPane appends create=1 to the terminal WS URL for flagged panes.
  • Host-service: the terminal WS attach handler's "no session row" branch calls the existing idempotent createTerminalSessionInternal when create=1 + workspaceId are present, deduped through the same in-flight map as respawns. Exited/disposed rows keep answering session-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=1 dial resolved to its existing id (attach, never a duplicate); pre-fix unflagged panes still dial create=0 with the adopt/respawn path intact
  • createOnAttach persists in the pane layout and is honored safely (only fires when no session row exists at all)

Testing

  • bun run typecheck
  • bun run lint
  • cd packages/host-service && bun test — 1065 pass, 0 fail
  • node --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.
  • createOnAttach deliberately 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 _whoowns preflight 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 httpLink in renderer/lib/host-service-client.ts (sidebar polls are the biggest steady-state consumer of the 6 sockets).
  • Keep minutes-long workspaces.create mutations from squatting pooled sockets.
  • plans/pty-lifecycle-cleanup.md item 9: park = disconnect, jitter + cap on resume-reconnect fan-out (the separate WS handshake-throttle contributor).
  • One-line finally hardening for the migration hook's runningRef (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 appends create=1. Failures show a disconnected pane with retry.
    • host-service: WS attach creates the session when no row exists and create=1 + workspaceId are provided; exited/disposed rows still return session-gone. Concurrent attaches are de-duped. themeType honored 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 .ts import extensions on the terminal.ts chain so node --experimental-strip-types tests run.

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.