Skip to content

fix(cmux-tui, cmux NEXT): the daemon answers a sidebar move promptly; a timeout toast says why in words - #34628

Merged
teamleaderleo merged 2 commits into
feat-cmux-nextfrom
fix/workspace-place-timeout
Nov 4, 2026
Merged

teamleaderleo merged 2 commits into
feat-cmux-nextfrom
fix/workspace-place-timeout

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, on the toast "Move did not apply: set-personal-workspace: timed out: workspace.place (no reply within 2.0 seconds)": "idk what happened here".

Cause

The app's log at the failed move shows every request to the home cmux-tui daemon timing out together (window_record.put, workspace.place, close-workspace, new-conversation-tab, put-frontend-projection, create-terminal). In the same minutes, the daemon logged slow journal maintenance: 5-8 s (slowest agent_roster) on every tick, about once a minute. So the daemon was blocked; the move itself was not slow.

The roster catch-up reads the journal tail except terminal.output with kind < 'terminal.output' AND sequence > ? on the (kind, sequence) index. A range on the first column cannot seek on the second, so each 512-record page walked every non-output entry the active journal held: 5.6M on this nightly registry. The fold lock and the I/O sit in front of agent hook appends and other requests, which then missed the 2 s deadline.

Fix

  • query_session_journal_without_output_after seeks once per kind: a recursive skip scan lists the distinct kinds from the same index (61 here), then joins a (kind = ?, sequence in window) seek for each. A bounded sequence window (8,192) caps how many matches one page sorts. A short page reports the window's end as covered. Every caller already loops until it reaches the head (roster fold, roster restore, history feed).
  • A command that misses its deadline now shows "cmux's background service was busy and did not answer in time, so it may still apply." (21 languages) instead of the command names. Callers (the control socket, the CLI) keep the raw error in message. This covers every action toast, not just Move.

Performance

metric before after delta harness
one 512-record non-output page, read-only on the 58 GB nightly registry (warm) 330 ms 5 ms -98% sqlite3 CLI, same rows returned
agent_roster maintenance step in the daemon 5-8 s per tick (daemon log) expected to be in milliseconds n/a the next nightly's client.log

Risk: SQL read path of the roster and history folds. The new test pages through 16k records with a run of output longer than one window and checks that each non-output record comes back exactly once, in order.

Tests: non_output_read_pages_every_other_kind_across_seek_windows (Rust) and DaemonTimeoutToastCopyTests (Swift).

Follow-up (not in this PR): the nightly registry is 58 GB with a 15 GB WAL. That growth is its own issue.

Agent session: agent session

🤖 Generated with Claude Code


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


Summary by cubic

Fixes the daemon answering slowly by making the roster catch-up's journal read seek once per kind, and replaces raw command names in timeout toasts with a human-readable reason.

Bug Fixes

  • The non-output journal read previously ranged on the (kind, sequence) index's first column, so each page walked every non-output entry the active journal kept (5.6M), blocking the daemon for 5–8 s per maintenance tick and making requests miss their 2 s deadline. It now lists distinct kinds via a skip scan and seeks per kind inside a bounded sequence window (8,192), cutting a 512-record page from 330 ms to 5 ms on a warm nightly registry.
  • A command that times out now shows "cmux’s background service was busy and did not answer in time, so it may still apply." in 21 languages instead of command names like set-personal-workspace: timed out: workspace.place. Callers keep the raw error in message.

Written for commit 3f34094. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 2 commits November 3, 2026 18:34
…daemon answers a sidebar move; a timeout toast says why in words

Leo: "idk what happened here", on "Move did not apply: set-personal-workspace:
timed out: workspace.place (no reply within 2.0 seconds)".

The roster catch-up in journal maintenance read the non-output tail with a
range on the (kind, sequence) index's first column, which cannot seek on
sequence: each page walked every non-output entry the active journal kept
(5.6M on the nightly registry), so the agent_roster step took 5-8 s every
tick, and requests behind it (workspace.place, put-frontend-projection,
history.trail.record, new-conversation-tab) missed their 2 s deadline.
The read now seeks once per kind (a skip scan over the same index) inside a
bounded sequence window: the same page in 5 ms instead of 330 ms warm.

A command that misses its deadline now shows "cmux's background service was
busy and did not answer in time. Try again." (21 languages) in place of the
command names; callers keep the raw error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
…stead of 'Try again' (review follow-up)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
@teamleaderleo teamleaderleo added the leo-named-fix An obvious fix to something Leo named: lands on push, review runs in parallel label Nov 4, 2026
@cursor

cursor Bot commented Nov 4, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent claude-opus-5-5 medium): SHIP at 561fe9c

Checked: reached semantics for every caller (roster fold, roster restore, history feed); the skip-scan SQL (the NULL kinds row, ordering, LIMIT, the archived prefix); that Swift message and mayHaveApplied are unchanged; that the xcstrings parse; the new Rust test's windows.

Follow-ups: [follow-up] the timeout copy said "Try again" while the change may still apply. Fixed at 3f34094, a copy-only change. [follow-up] the history feed saves on every page across long runs of output. [follow-up] the distinct-kinds read could be cached per catch-up loop.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 3f34094: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr34628 -f ref=fix/workspace-place-timeout -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on 3f340947de; ci-status is green without them.

CI passes on 3f340947de (run 41761998536 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

Shipped in:

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@teamleaderleo
teamleaderleo merged commit 5b374e5 into feat-cmux-next Nov 4, 2026
114 of 153 checks passed
@teamleaderleo
teamleaderleo deleted the fix/workspace-place-timeout branch November 4, 2026 02:41
teamleaderleo added a commit that referenced this pull request Nov 4, 2026
Conflict in ActionWorkFailure+Daemon.swift with #34628, which gives a missed
deadline a reason in words. Both kept: the reason stays on the failure, and
notice(title:) shows nothing for a missed deadline (Leo: no chatty
unactionable toasts). A close that may still happen (#30915) keeps its notice.

Sidebar-Look: no visual change
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-cb01117327fc358e9ba1
teamleaderleo added a commit that referenced this pull request Nov 4, 2026
Leo: "app's calls to the daemon were timing out at 2 s. shouldn't be this
short. no reason for this."

cmux-tui's control-plane default (DaemonConnection.defaultRequestTimeout)
goes from 2 s to 30 s, the heartbeat's not-answering window (15 s x 2): a
busy daemon answers late (#34628's 5-8 s journal step failed requests it
still answered), and a daemon that misses that long is the heartbeat's
not-answering case with its Restart offer. Snapshots (10 s) and the loopback
forward client follow it. The app's acpmux client reads and writes use one
AcpmuxEnvironment.answerDeadline (30 s) in place of their 2 s defaults.
Quit and shutdown bounds keep their 2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-47ff611049541adb5070
teamleaderleo added a commit that referenced this pull request Nov 4, 2026
…i and acpmux clients) (#35075)

* test(daemon): a daemon that answers in 3 s is answered, not timed out (red on base)

Leo: "shouldn't be this short. no reason for this." on the app's calls to
the daemon timing out at 2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-47ff611049541adb5070

* fix(cmux NEXT): the app waits 30 s for a daemon's answer, not 2 s

Leo: "app's calls to the daemon were timing out at 2 s. shouldn't be this
short. no reason for this."

cmux-tui's control-plane default (DaemonConnection.defaultRequestTimeout)
goes from 2 s to 30 s, the heartbeat's not-answering window (15 s x 2): a
busy daemon answers late (#34628's 5-8 s journal step failed requests it
still answered), and a daemon that misses that long is the heartbeat's
not-answering case with its Restart offer. Snapshots (10 s) and the loopback
forward client follow it. The app's acpmux client reads and writes use one
AcpmuxEnvironment.answerDeadline (30 s) in place of their 2 s defaults.
Quit and shutdown bounds keep their 2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-47ff611049541adb5070

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…e (cmux-ad m11 2) (#35329)

* bench: the daemon's request latency through a journal step, and head-of-line (cmux-ad m11 2)

Two scenarios for cmux-tui/scripts/bench-daemon-latency.py (what
scripts/measure/perf-10x-daemon.sh runs), not in the default list. The
set DOGFOOD BENCH runs per build:

  cmux-ci run --class exclusive --measurement \
    --script scripts/measure/perf-10x-daemon.sh --ref SHA \
    --arg=scenarios=small,journal-step,head-of-line --timeout 3600

- journal-step: 80 agents' real hook helpers fill the journal while an
  app connection (put-frontend-projection, list-workspaces, identify)
  and a churn connection (create and close terminals) run through
  --journal-ticks maintenance ticks (30 s apart in a release build).
  It reports each kind's p50/p95/p99/max, the requests over 30 ms, and
  the daemon's slow-maintenance lines. This is #34628's 5-8 s stall,
  for which #35075 raised the app's deadline to 30 s.
- head-of-line: the same load, with an identify that shares one
  connection with the churn, against the same identify on its own
  connection. These are the data for per-request dispatch, an open
  question for PL-daemon.

* bench: start the daemon as the app does, with --terminal-reap-grace-seconds 30

Without the flag a close leaves the terminal running unplaced (reaping is
opt-in, main.rs), so journal-step's ~350 creates and closes and then
head-of-line's terminals ran the Mac out of PTYs (511 on macOS): "PTY capacity
exhausted" on aws-m4pro-9 (727887e6) and on aws-m4pro-8 (795b1221) at the same
point, with ~550 terminal hosts still alive at the end of the step. The app's
DaemonLauncher passes 30 s; at most 16 closed terminals then wait it out.
--reap-grace-seconds sets it; a negative value leaves the flag out.

* bench: each churn scenario ends its agents; a refused connection or a dead worker fails the run

On cmux17-perf (fleet 6aca4d0c, b34023f1) head-of-line recorded nothing on its
shared connection: journal-step's 80 agents were still running, so 160 agents'
hook helpers and CLI calls took every connection the daemon gives terminal
processes, and the daemon refused the bench's own connections at its limit
(MAX_SERVER_CONNECTIONS). The refusal line has no id, so Conn.call read on to
the close; the worker threads died, and the run still exited 0 with n=0.

- journal-step and head-of-line start their agents with a stop file and end
  them (the stop file, then their surfaces closed) before the next scenario.
- Conn.call says "refused: <code>: <message>" for the daemon's refusal line.
- A head-of-line worker that fails stops the scenario, and an error in either
  scenario fails the run after its numbers are printed. Nothing is retried.

* bench: head-of-line measures the daemon's queue, not the bench's lock

Review FIX at 4acfccb: head-of-line held its own lock across each shared
call, so the shared identify went out only after the create's reply; its
~100 ms p95 was the bench's wait. A plain identify never waits in the
daemon's queue (connection_frame.rs answers it on the reader), and a create
leaves the connection's dispatch thread for the terminal pool.

- head-of-line keeps several requests in flight per connection
  (PipelinedConn): a request goes out at once, one reader thread per
  connection matches each reply by id, and each request is timed from its
  send to its reply. One connection sends close-surface (it runs on the
  connection's dispatch thread) and right behind it list-workspaces (queued
  behind the close) and identify (answered on the reader: the control); a
  second connection sends the same two at the same moment. Every reply goes
  through must().
- journal-step: a worker whose connection fails to open, or that gets an
  error reply, stops the scenario and fails the run; a failed request is no
  longer counted as a sample. create-terminal is reported apart, with
  create-terminal-idle (20 creates before the agents start), and kept out of
  "all" and over-30ms.
- The window holds --journal-ticks whole ticks: 30 s per tick, plus
  --journal-tick-seconds (default 10) for each tick and for the one running
  when the window opens.
- The agents scenario ends its agents with a stop file, as journal-step does.
- The docstring says which requests skip the queue, that the reap grace
  changes the default scenarios' new-tab numbers, and that a tick under
  500 ms logs no line.
cmux-bot Bot pushed a commit that referenced this pull request Nov 4, 2026
…x-ad m11 2) (#35334)

* cmux-tui: requests wait for no roster catch-up, test (red, cmux-ad m11 2)

A journal maintenance tick's roster catch-up holds the roster fold lock
for its whole journal tail read and snapshot write
(agent_roster_fold.rs advance_agent_roster), and request paths take
that lock: a terminal close (lifecycle.rs purge_terminal_side_tables),
every agent journal ingress (journal.rs finish_journal_ingress ->
fold_agent_roster) and a direct agent report's echo (agent_reports.rs).
Requests on one connection run in order on its one dispatch thread, so
one of them waiting there held all later requests of the app: the
5-8 s step of #34628, and #35075's 2 s -> 30 s app deadline.

The test pauses a catch-up mid-tail (the existing page seam) and asserts
that a workspace close and an agent report finish within 500 ms. Red
here: both wait for the catch-up.

(cherry picked from commit c45ef0b)

* cmux-tui: the fold-hold test reports a working agent (AgentState has no Running) (red)

c45ef0b did not compile (fleet 77b55788: E0599, no variant Running);
the agent states are working, blocked, idle, done and unknown.

(cherry picked from commit aab2df2)

* cmux-tui: no request waits for a roster fold another thread runs (cmux-ad m11 2)

The test is the commit before this one. Request paths (a terminal
close, every agent journal ingress, a direct agent report's echo) took
the roster fold lock, which a maintenance tick's catch-up held for its
whole journal tail read and snapshot write: 5-8 s on #34628's nightly
registry, and one request waiting there held every later request of its
connection (one dispatch thread per connection), which is why
workspace.place, new-conversation-tab, window_record.put and others
timed out together and #35075 raised the app's deadline to 30 s.

Now a request records what it needs (fold through its commit, or
retire a closed terminal) behind a short lock of its own
(`Mux::agent_roster_requests`) and `try_lock`s the fold: when another
thread holds it, the request returns at once, and the holder does the
work before it lets go (it reads the requests again after each page and
after it lets go, so none is left behind). The catch-up keeps the only
blocking wait, on the maintenance thread. A retirement now waits until
the roster has folded the journal head, so the closed terminal's own
earlier events cannot bring its entry back. Uncontended, a request
folds exactly as before.

(cherry picked from commit e9ba248)
(cherry picked from commit ec3b321)

* cmux-tui: the roster's request queue lives in its host, so mux.rs does not grow

mux.rs is over its 1000-line budget on the base (1114 lines); the first
cut added a Mux field. The queue now sits in AgentRosterHost, whose lock
is the short in-memory lock every fold already takes per page.

(cherry picked from commit 830d8b7)
(cherry picked from commit b4364bb)

* cmux-tui: the fold-hold test drives the request paths that take the fold

Red2 (fleet 99500a98, hq run 41813886487, at aab2df2) passed on the
old code: a workspace close does not purge its terminals on the request
thread (tree_close.rs ends with purge_surface_side_tables only), and a
direct report's journal echo may not reach the fold. The test now uses
the two paths that do take the fold lock: an agent hook through
append_journal_ingress (session.journal.append) and the terminal purge
every terminal close runs (purge_terminal_side_tables). It also asserts
what they asked for is done once the catch-up lets go: the roster at
the journal head, the closed terminal's entry gone, the hook folded.

(cherry picked from commit 9a8f124)

* cmux-tui: the fold-hold test uses the imported HashMap (unused_qualifications)

(cherry picked from commit c68a3e4)

* threading-model: per-request dispatch on a daemon connection, an open question for PL-daemon (cmux-ad m11 2)

Lead condition 2: one slow request on a connection's single dispatch
thread holds every later request of that connection. Not changed here;
the bench's head-of-line scenario (scripts/measure/perf-10x-daemon.sh
--arg=scenarios=head-of-line, its own scripts-only PR) gives the data.

* test(cmux-tui-core): roster work a request records is never left behind, and no close or hook reads the journal or saves the roster on its own thread (#35334 review F1, F2; red)

Regression tests for the review of #35334 (they fail on the code before the fix):

- F1: a hook recorded while another thread runs a roster pass that makes no
  progress (a retirement of a terminal with no entry, the roster at the head)
  is left behind: that pass returns without reading the requests again.
  The reviewer's repro (f65ccd3), without reading internal request state.
- F2 (perf-lead.md D1): a terminal close or an agent hook that finds the fold
  free reads the journal tail and saves the snapshot on its own request thread.
  One test holds whichever thread reads the first page and puts a 500 ms
  budget on the close and the hook; one records the thread of every roster
  journal read and snapshot save and asks that all are the roster's owner
  thread (cmux-agent-roster).
- P3: the catch-up test asks for Some("working"), not is_some().

Test seams (cfg(test)): a pause after the head read (from the review), the
threads of the roster's journal reads and saves, and a settle wait (nothing
to wait for here: every request does or hands off its work before it returns).

* fix(cmux-tui-core): one roster owner thread does every fold and snapshot save; a request only records its work (#35334 review F1, F2)

The review of #35334 found two defects in its hand-off: a request recorded
while another thread ran a pass that made no progress was left behind (F1),
and a terminal close or a hook that found the fold free read the journal and
saved the snapshot on its own request thread (F2, perf-lead.md D1). The root
fix the review recommended: one background owner.

- mux/agent_roster_worker.rs: the `cmux-agent-roster` thread owns the fold.
  A request (hook ingress, terminal close, direct report echo) records what
  it needs under the owner's own lock (the highest sequence asked, the
  closed terminal) in O(1) and wakes it; it never folds, reads the journal
  or saves. The owner takes every recorded request before each pass and
  looks again after it, so nothing is left behind (F1); no request thread
  reads or writes for the roster (F2). It sleeps on a condvar (no polling)
  and holds only a weak mux reference between passes.
- The maintenance tick's catch-up asks the owner for a pass through the
  journal head and waits for it, so the tick's checkpoint and seal still
  come after the roster caught up. Shutdown lets the owner finish the work
  recorded before it (retirements are saved) and waits at most 2 s.
- mux/agent_roster_fold.rs `run_roster_pass`: a retirement runs only after a
  head read in the same pass and a fold that reached it; when the head or a
  page cannot be read, it waits for the next pass (P3 F5). A pass that moved
  nothing saves nothing and serializes nothing (P3 F3). A panicking pass
  does not end the owner.
- The live agent count of the activity stream is read again after a pass
  that changed the roster (the request that changed it answered first).
- The roster host goes back to the base shape (no request queue in it).
- Default QoS, as the journal writer, maintenance and history feed threads
  (threading-model.md): its passes hold the roster lock, the writer's
  connection lock and (plugin deltas) the registry lock that requests take.

Tests: the three regression tests of the previous commit go green; tests
that read the roster right after a write now wait for the owner first
(`settled`), since the owner folds after the request answered.

Red (the previous commit on the code before this fix): fleet step
1dc37bc961cd75fa606dd127 (aws-m4pro-4), 3 of 5 red, also alone.

* test(cmux-tui): the sidebar projection tests wait until the roster lists a reported agent

The agent roster's owner thread folds a report right after the report
answered (#35334), and the sidebar reads the roster, so a test that reports
an agent and then renders or navigates its row waits for the roster first.
These tests build against cmux-tui-core without its test seams, so they wait
on the public list_agents read.

* test(cmux-tui-core): the roster regression tests check the hook's own sequence, not the journal head

A hook commits its record and then its projection's resource change, so the
journal head is one record past what the hook asks the roster to fold. With
one owner, nothing asks for that later record until the next request or the
maintenance catch-up, so the F1 and F2 tests now check that the roster folded
through the hook's own sequence and that the saved snapshot is at the roster's
cursor. Their first failing check on the code before the fix is unchanged.

* threading-model: the head-of-line probe of the repaired bench (#35329) and its numbers

The probe is a list-workspaces sent right after a close-surface on the same
connection (pipelined), against the same request on a connection of its own,
with identify as the control (cmux18-perf, fleet ce07a34858265193980f9cdf).

* test(cmux-tui): the roster wait helper names SurfaceId, Instant and Duration as the module imports them

clippy -D warnings (unused_qualifications) refused the fully qualified paths.

* threading-model: the roster owner stays at the default priority because a throttled holder of locks that requests take is a priority inversion

This branch was previously deployed

3 inactive deployments
artifacts — 3f340947 Deployed Nov 4, 2026 by teamleaderleo via publish tree-addressed cmux-tui #108877
sentry-dsyms — 3f340947 Deployed Nov 4, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #108877
ci-routing-agent — 3f340947 Deployed Nov 4, 2026 by teamleaderleo via cmux-tui tree preflight #108877
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

leo-named-fix An obvious fix to something Leo named: lands on push, review runs in parallel risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant