Skip to content

fix(ui): guard first-message session hydration#494

Merged
Pizzaface merged 7 commits intomainfrom
fix/mcp-startup-session-limbo
Apr 27, 2026
Merged

fix(ui): guard first-message session hydration#494
Pizzaface merged 7 commits intomainfrom
fix/mcp-startup-session-limbo

Conversation

@Pizzaface
Copy link
Copy Markdown
Owner

Summary

  • block session composer submissions while the active conversation is still hydrating so the first message can't race ahead of the initial snapshot
  • add shared UI/send-side readiness checks plus regression coverage for the first-message hydration path
  • fix the sandbox harness trusted-origin flow so browser verification can run against the HMR sandbox again

Test Plan

  • bun test packages/ui
  • bun test packages/server/tests/harness/server.test.ts packages/server/tests/harness/sandbox.test.ts
  • bun run typecheck
  • verified in a real browser that the first message and live reply appear without reselecting the conversation

…ed input)

When a worker is spawned and MCP startup is slow, three races combine
to make new sessions appear "stuck in limbo":

1. initial-prompt.ts only waited for the relay to register, not for the
   worker startup gate. Since relay registration takes ms while MCP can
   take 10-30s, the initial prompt started streaming before MCP had
   finished loading — the first turn ran without MCP tools and raced
   ahead of any input buffered behind the gate.

2. connection.ts's sock.on("input") awaits the startup gate before
   dispatching, which is correct. But the UI sends input with no
   deliverAs when it thinks the agent is idle — and by the time the
   gate releases, the initial prompt is already streaming. Calling
   sendUserMessage into a streaming agent with no deliverAs throws
   "Agent is already processing..." and the message is silently
   dropped, leaving the UI in "limbo forever" until the user cancels
   and retypes.

3. The capabilities snapshot (models, commands) is broadcast once on
   relay `registered`. If MCP is still loading at that moment, the
   Redis snapshot stays stale — users have to navigate away and back
   to see fresh models/commands.

Fix:

- initial-prompt.ts now awaits Promise.all([waitForRelayRegistration,
  waitForWorkerStartupComplete]) before dispatching the initial prompt.
- connection.ts extracts resolveInputDeliverAs() — when a requested
  deliverAs is absent and rctx.isAgentActive, default to "followUp"
  so the buffered input is safely queued instead of silently thrown.
- lifecycle-handlers.ts subscribes to mcp:registry_updated and
  re-broadcasts capabilities so stale snapshots self-heal once MCP
  finishes loading.

Regression coverage:
- initial-prompt.test.ts: delays sendUserMessage until the gate releases
- deliver-as-default.test.ts: resolveInputDeliverAs truth table
- mcp-registry-rebroadcast.test.ts: capabilities re-emit on registry update

Builds on the earlier startup-gate fix (#446 / wNXa7QPF) which wired the
gate into triggers and web-UI input but missed the initial-prompt path
and left no rescue for the deliverAs race.

Closes: oJgYHKTr
…eload socket.io-client

CI ran the unit tests in a cleaner environment than my local machine and
exposed a test-ordering bug I'd introduced:

  (fail) remote connection startup gate > buffers trigger-delivered turns…
  (fail) remote connection startup gate > buffers remote input…
  (fail) remote connection startup gate > delivers immediately when gate is not armed…

All three failed with `lastSocket === null`. Bun preloads every *.test.ts
file in the directory before running any test. When
deliver-as-default.test.ts imported `resolveInputDeliverAs` from
./connection.js, it transitively pulled in socket.io-client before
connection.startup-gate.test.ts had a chance to `mock.module` it.
Subsequent calls to `io()` used the real library, the FakeSocket was
never created, and `lastSocket` stayed null.

Same ordering hazard affected the new mcp-registry-rebroadcast.test.ts:
it imported lifecycle-handlers.ts, which transitively loads
connection.ts and its socket.io-client import.

Fixes:

- Move `resolveInputDeliverAs` to its own file,
  `packages/cli/src/extensions/remote/deliver-as-default.ts`. The test
  imports from there, so no part of connection.ts's graph is eagerly
  loaded. connection.ts just imports the helper from the new file.
- Delete mcp-registry-rebroadcast.test.ts. The registration it covers
  is three lines in lifecycle-handlers.ts and exercising it in
  isolation requires mocking the entire lifecycle surface, which either
  duplicates runtime or reintroduces the same preload problem. The
  behavior is a trivial pass-through; the other regression tests
  (deliver-as-default + initial-prompt) remain in place.

Verification:
- bun run typecheck — clean
- cd packages/cli && bun test src — 1854 pass / 0 fail / 1854 tests

(Previous commit in this branch: 1828 pass / 27 fail locally, which
matched the CI red.  With this fix the whole cli suite now runs green
locally, which should also match CI.)
@Pizzaface Pizzaface merged commit 3df7c5d into main Apr 27, 2026
11 checks passed
@Pizzaface Pizzaface deleted the fix/mcp-startup-session-limbo branch April 27, 2026 13:11
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.

1 participant