acpmux ls: read the session list in pages (never one reply), --cursor, titles at most 200 chars
Summary
acpmux ls now reads the session list in pages and never asks for it in one reply, so a host's list size no longer matters.
The failure this fixes, as reported from the fleet:
on a fleet host (acpmux c230bb58a), acpmux --json ls fails with {"error":{"code":"runtime","data":{"bytes":15744335,"limit":15728640,"reason":"message.too_large"}}}. ~3,700 sessions with every peer's rows, ~4 KB each, because titles carry whole prompts. --fields, --status, --since and name patterns filter on the client after the daemon sent the whole list; --limit pages in the daemon, but ls takes no cursor.
Pages for every read. ls reads _acpmux/sessions 1000 rows at a time with limit + cursor (cli/run/ls_pages.rs) and prints the same {"sessions":[...]} as before.
- This works against old daemons too: any daemon since the keyset pages (the reported c230bb58a included) answers
limit. A daemon too old forlimitsends the whole list, which is read as before. - A page too large to send is asked for again at half the rows. If a single row is too large, the error says so and points at
session infoinstead of a baremessage.too_large. - A row that a later page repeats is kept once. A cursor that comes back twice is an error, never a loop.
Filters run in the daemon before it pages.
--statusis sent asstatus(exceptstuckandwaiting, which are not daemon statuses).--liveis sent asexcludeStatus.--host <this machine>is sent aslocalOnly.--sincestops the read at the first older row; the list is newest first, so noupdatedSinceparam is needed.- A peer's
--hoststays a client-side filter.--hostmatches peer names in any case, but the daemon'speerfilter compares case-sensitively, and peer names may be mixed case. Sending it could silently drop rows.
Scripts can page.
ls --cursor C [--limit N]reads one page and prints itsnextCursor(null when nothing is left). An empty--cursor ''is the first page.--limit N --jsonalso prints the cursor for the rows after its N. When a client-only filter (pattern,--tag,--pending,--status stuck|waiting) stops in the middle of a page, the cursor is the one for the last row read, built the way the daemon builds it (session_pages::cursor_after). If a daemon's cursor keys differ, the read starts at the first row no newer than that one, so a row from the same millisecond may repeat but none is skipped.- A daemon too old for pages ignores
cursor.--cursoragainst it is refused with "too old to page: update acpmux on it" instead of returning the list from the start.
Titles are bounded in census rows. A census row (session_list_summary) shows at most 200 characters of its title, the same cut lastPrompt gets. A harness may title a chat with its whole first prompt. Census rows are both the list and the session_changed rows a watch without tools hears, so the two never show one chat under two titles. The Mac's workspace auto-naming reads them, and a cut in only one would rename a workspace back and forth. session info, attach and --with-tools keep the whole title. I checked the consumers: nothing compares, writes back or keys on a list title. lastTurn.errorText and stuckError are not cut, because leo-workspace's keep_going_reconciler.py, pr_owner.py and lane_failover.py regex-match them from acpmux --json ls.
Prior art and overlap
- #28182 (open, same area) pages a whole list on the daemon side when it is too large, and switches
lsto itsClient::whole_list. That fixes new daemons. This PR fixes the CLI against the daemons running now, adds--cursor, and bounds titles.- The two touch one line of
ls.rs: #28182'swhole_listcall becomes this PR's paged read. Whoever lands second keeps the paged read; it reads #28182's daemons the same way, since a request withlimitstill pages by count there. - Request 4 of the brief (a whole-list reply over the cap refused with a clear error) is left to #28182. A whole list that is too large goes in pages there instead of being refused, so this PR does not change that daemon code.
- The two touch one line of
- Related: HQ#5226; the fleet-scale paging PRs #28152 #28172 #28170 #28157 #28161; and #28272 #28273 #28296 #28297 (the clients read a paged list whole).
Performance
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| largest reply, 3,700 sessions of ~4.4 KB | 16.2 MB (refused: over 15 MiB) | 4.4 MB (pages of 1000) | -73%, and the list is read | ls_pages_tests::a_busy_hosts_list_goes_in_pages_under_the_message_limit (fake daemon over the hub's session_list_reply) |
| requests for that list | 1 (fails) | 4 | +3 | same test |
| this lane host's real list, 417 rows | 1,138,925 B | 1,137,005 B with titles cut | -0.2% | acpmux --json ls on a lane VM, titles cut to 200 chars offline |
The title cut barely changes rows on this host, because its titles average 58 bytes. I could not read the reporting host's rows from here, so its "titles carry whole prompts" estimate is not measured. Paging fixes the cap however large the rows are.
Testing
cmux-ci test -- cargo test -p acpmux --lib -- ls_pages summary_title cli::run::ls:: hub::session_pages hub::views, thencargo clippy -p acpmux --all-targets --locked -- -D warnings. Run on Linux fast checks at each head; results are in the comments.- Round 1 at ea8e062cc8f: 12/12 tests passed. Clippy failed on a dead-code field the tests read (
Pages.requests); it is fixed in db6c7bcdcdc. - An independent Codex review (gpt-6-astra, high) at ea8e062cc8f blocked on three findings. All three are fixed in db6c7bcdcdc: the mid-page cursor,
--cursoron an old daemon, andsession infokeeping the whole title.
Observed / Not run
- Observed: the field sizes of a real list (
acpmux --json ls, 417 rows; the largest fields arelastPrompt/lastTurn/enforcement, about 200 B each). Titles there are already short. - Not run: the new
lsagainst the reporting host's daemon. The fake daemon in the tests is the hub's own paging function.
Changelog
- Fixed:
acpmux lsworks on hosts whose session list is larger than one message, and--cursorreads it page by page.
Agent session: agent session
š¤ Generated with Claude Code