cmux-next: a new chat's first message keeps its Enter's gesture through a slow harness start

AU
4d ago
+323-14

Fixes #23031

Summary

A new chat's first message often came back as the bubble "Not sent. Press Retry to send it." That bubble comes from one refusal only: the app's relay answering transport.gesture_required to a session/prompt that no composer holds (direct.ts, the failed-row branch).

Root cause: a harness pick (New Tab, the harness picker, a model row) starts the new chat right away (harnessSwitch.ts switchTo → run → session/new). A prompt the person sends while that harness starts is queued in the switch and sent by run() once the session opens, with no gesture ticket. The relay then grants it only if:

  • the Enter's gesture is still live (AgentPaneUserGestures.lifetime, 30 s), or
  • the start credit from the session/new is still good (startCreditLifetime, 120 s from the pick, and only if the pick's gesture was live when session/new arrived).

A cold harness start (first chat after launch, a harness probing its backend) outlasts the Enter's 30 s, and the pick's credit is often gone or was never given. So the prompt is refused. Because the switch's prompt has no composer holding it, the refusal shows as the failed bubble with Retry, and the Retry click (a fresh gesture) then works. https://github.com/manaflow-ai/cmux/pull/22143 covered only the case where session/new goes out under the send's own gesture (Enter in an empty chat). It didn't cover a start that began at the pick.

Fix: when the switch holds a new chat's first prompt, it reserves that prompt's session/prompt gesture ticket in the Enter's own handler (transport.gesture, the mechanism trust-held prompts already use, heldPrompt.ts; 600 s lifetime). run() sends the prompt with that ticket. Exceptions:

  • A seeded prompt (it has its own host ticket) takes no gesture.
  • A chat on another machine that hasn't started yet takes no gesture, because its start needs the Enter's gesture.
  • A ticket taken on an earlier connection isn't sent after a reconnect (tickets are per connection); that prompt falls back to a live gesture, as before.
  • When a trust refusal comes later, fail() doesn't hold the prompt a second time when it already has a ticket.

The relay's rules are unchanged.

No capture: the fix changes when a held first prompt is delivered (send timing during a harness start), not any resting UI state, so a tour of the app's states would show nothing new. The real before/after of the scenario (base 92d14b214ffe shows "Not sent. Press Retry to send it.", this PR delivers the prompt) are tagged fleet-build window snapshots in https://github.com/manaflow-ai/cmux/pull/23103#issuecomment-6310565159.

Defaults chosen (no questions asked)

  • Ticket only for the first held prompt. The relay keeps one session/prompt ticket per connection. A second prompt queued behind the same start still needs a live gesture (follow-up).
  • No ticket in three cases: a seeded prompt (it has its own host ticket), a send that restarts a failed switch (its session/new may need the gesture for its folder), and a remote chat that has not started (its start needs the gesture).
  • Only the person's own send takes a ticket (byPerson, set by App for everything but onboarding's seed), and not a send in the same turn as its harness pick (the New Tab page's pick and Send): that session/new takes the Enter itself and carries it as the 120 s start credit, so its folder check keeps the gesture it needs.
  • Latest results are in https://github.com/manaflow-ai/cmux/pull/23103#issuecomment-6390797377 (head 43cc80e).
  • Reconnects: a ticket from an earlier connection is not sent on a new one; that prompt falls back to a live gesture, as before.
  • Start credit: a ticketed first prompt spends the start's credit (AgentPaneTransport+Frames.swift), so one gesture grants one prompt. This file puts the PR in risk:high (gesture rule); it clears with an independent review receipt on the exact head, posted on the PR.
  • Capture: a No capture: line (above) instead of a tour, because nothing at rest changes.
  • Base merges: feat-cmux-next was merged in (no conflicts); base reds are listed below and not fixed here.

Testing

  • webviews/test/switch-held-prompt-keeps-gesture.test.ts: the switch's first held prompt goes with the ticket its Enter took; only the first takes one; a seeded prompt and a not-yet-started remote chat take none; a ticket from before a reconnect isn't sent.
  • AgentPaneSlowStartPromptTests.aPromptHeldBehindAStartThatOutlastsTheGestureGoesWithTheTicketItsEnterTook: at the relay boundary, the issue's timing (start at the pick, Enter 20 s later, start answering 130 s after the pick). Without a ticket the prompt is refused (the bug). With the Enter's ticket it's sent once, and a replay is refused.

Red/green, on the fleet (cmux-ci run; Freestyle isn't configured on this Mac):

  • scripts/ci/run-webviews-tests.sh at the test-only commit 4d00a1c74dc2 (step e71772795d04): test/switch-held-prompt-keeps-gesture.test.ts fails, 3 fail and 2 pass. The 2 passing tests are the exclusion guards (seeded prompt, remote start), which pass both before and after by design.
  • At the first fix head 8e91dfc22ee0 (step b7e88e46ae30): it passes. The remaining failing files are the same 18 browser-lane files as at 4d00a1c, and this PR doesn't touch any of them.
  • At the current head e61b6d7f7dc4 (step 835df9974bff): the new test, held-mode-refused-notice and new-tab-folder-keeps-pick pass. Many unrelated files fail on that worker (cmuxs-Mac-mini-3) because of its environment: a work path with a space breaks the tests' URL.pathname lookups, and Playwright WebKit isn't installed.
  • scripts/ci/run-swift-testing-suites.sh Packages/macOS/CmuxNext (4 shards) at 8e91dfc: AgentPaneSlowStartPromptTests passes. The failing suites are all outside this change (app UI, sidebar, settings schema export, and AgentPaneImageActionsTests timing out after 93 s). At 4d00a1c only SettingsSchemaExportTests failed.

Observed / Not run

Observed:

  • The fleet steps above.
  • A GUI repro on two tagged fleet builds driven over the debug socket without taking focus, with a stub ACP harness whose session/new takes 45 s:
  • bun run check (vp check) fails only on an existing jsx-a11y warning in src/pages/history/HistoryPage.tsx, the same on base. prepush-gate.py reports no new violations.

Not run / only read:

  • Real harnesses (Claude, Codex) starting slowly. The stub stands in for them.
  • The second-prompt, remote-over-120-s and reconnect cases listed in the review comment, which this PR does not cover.

Changelog

Fixed: the first message in a new agent chat no longer fails with "Not sent. Press Retry" when the agent takes a while to start.

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes #23031: a new agent chat's first message no longer fails with "Not sent. Press Retry" when the harness is slow to start.

The harness switch held a prompt sent while the chat started and sent it without a gesture ticket once the session opened. The relay then required the Enter's gesture (30 s) or the pick's start credit (120 s), and a cold harness outlasts both. The switch now reserves the send's gesture as a session/prompt ticket (600 s lifetime) and sends the first held prompt with it.

  • Only the first held prompt takes a ticket; seeded prompts, not-yet-started remote chats, and sends that restart a failed switch don't.
  • A ticketed prompt also spends the start's credit, so the credit never grants a later prompt.
  • A ticket taken before a reconnect isn't reused.

This branch also carries the feat-cmux-next merge: subagent workspaces move into the user's session, the sidebar row's hover card aligns to its row's top, suggested tasks default on (sessions.promptSuggestions), and failed sends group by cause.