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

Merged
TE
2d ago
+271-20

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

metricbeforeafterdeltaharness
one 512-record non-output page, read-only on the 58 GB nightly registry (warm)330 ms5 ms-98%sqlite3 CLI, same rows returned
agent_roster maintenance step in the daemon5-8 s per tick (daemon log)expected to be in millisecondsn/athe 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.