acpmux: every session list page and session info fits one message (ls never hits message.too_large)
Summary
On a fleet host, plain acpmux ls fails: acpmux: runtime: message too large to send: 16927852 bytes, the limit is 15728640. No one can list that host's sessions, and the census and stall reconciler can't see it either. The CLI only retries with smaller pages, so the daemon has to send every list reply whole and under the cap.
Where the daemon still built a reply too large to send:
limitpages were cut by row count only.acpmux lsreads pages of 1000 rows with usage (#28373). The census'sorderBypages and everywithToolspage are cut the same way. A page of large rows came to 37 MB in the test below. The byte-bounded paging from #28182 covered only replies with nolimit.- One large row went whole into a
limitpage. On the byte pages it became an id-only stub. Every field was lost, itscwdand status included. _acpmux/infosent a detail of any size, for example a chat whose title is a whole pasted prompt.
The fix fits every reply as a whole and never refuses one:
- New
hub/reply_fit.rsholds the shared rule and bound.shrink_largestreplaces the largest parts first until the value fits.REPLY_BYTESis 1 MiB underrpc::MAX_MESSAGE_BYTES.fit_objecttrims a row's largest fields: a string keeps a 1 KiB preview, any other value is dropped. It never trims the fields a row is found and opened by (sessionId,id,qualifiedSessionId,sessionReceipt,peer,status,harness,cwd,updatedAt,name; the same set #28754's attach reply keeps), and it marks the rowomitted: {bytes, reason: "too_large", fields: [...]}.LIST_PAGE_BYTESis nowREPLY_BYTES. Snapshot::page(thelimitpages and census pages) holds at mostlimitrows and at mostREPLY_BYTESbytes. A page cut short by size returns thenextCursorof its last row (k…, ors:<id>for a census), and clients already follow it.withToolspages use the same path. They no longer parse a copy of the whole list.- A row too large for a page by itself is trimmed, on the byte pages too. Only a row whose kept fields alone are that large still goes as the id stub.
_acpmux/inforuns its detail throughfit_reply_object.- A trimmed row still reaches a peer masked (
peer_text.rs).lastSeqis a kept field, since masking recognizes a summary bysessionId,harnessandlastSeq. A preview is masked over 4 KiB past its cut and only then cut, the wayviews.rsmasked_cuttreats titles, so a secret split by the cut is never left as a head.
#28754 adds the same kind of fit for the chat-open reply (_acpmux/attach). The two are built on one rule: paging::fit_reply there is shrink_largest over /events/N/msg and /turnOpener/msg. Whichever PR lands second switches to the shared helper and the single bound.
Related: #28182, #28373, #28161, #28105 (the earlier paging work).
Testing
server/list_fit_tests.rs (new) builds a hub with 1,000 peer rows of 20 KB plus one 16 MiB row, and a local chat with a 16 MiB title. The list as a whole is past the cap, and so is one row alone. Through a real server connection:
_acpmux/sessionswith{limit: 1000, usage: true},{limit: 1000, withTools: true},{}and a census (orderBy): followingnextCursorto the end lists every id once, and every reply line is underMAX_MESSAGE_BYTES. The large row keeps its id, peer, status andcwd, itsplanStepbecomes a preview, and it is markedomitted.fields = ["planStep"]. Small rows are unchanged._acpmux/infoon the 16 MiB-title chat answers under the cap, with a title preview andomitted.fieldsnamingtitle.- Over a peer connection (masked), an oversized peer row whose
lastPromptholds one token near its start and one split at the 1 KiB cut keepslastSeq, and neither token's start appears in the preview (a_trimmed_row_still_goes_to_a_peer_masked). Withreply_fit.rsas of 468e477 this test fails: the split token leaked asghp_CutkQ9mX7pL2vB8n….
Command: cargo test -p acpmux --lib -- list_fit (in cmux-tui/).
- Base (test commit 2b4c96cc8f6 alone): both fail. The first page fails with
{"limit":1000,"usage":true} page 0: message too large to send: 37190897 bytes, and info fails withmessage too large to send: 16779433 bytes. - Head: all pass. With
-- list_fit reply_fit list_guard list_reply acp_list session_pages ls_pages native_history peer_text: 59 passed, 0 failed.cargo clippy -p acpmux --lib --tests -- -D warningsis clean.
Observed / Not run
- Observed: the red/green above, run locally on a Linux lane VM (
cargo test -p acpmux --libwith a filter). Panic ratchet (rust_panic_ratchet.py,-p acpmux): arithmetic_side_effects and cast_possible_truncation report above baseline, but none of the hits in the changed files are on lines this PR adds (they are existing base code). - Not run: the full acpmux suite here (CI runs it), and a live
acpmux lson the failing host. That host runs an acpmux older than #28373 (its error is the raw daemon message, which the paged CLI would have caught and retried smaller), so it needs an update that includes this change first.
Changelog
Fixed: acpmux ls and acpmux session info work on a host whose session list, or one session, is larger than one message; an oversized field shows as a preview and is marked omitted.
Proof
No UI: daemon reply sizes only. The tests above show it.
| metric | before | after | delta | harness |
|---|---|---|---|---|
first _acpmux/sessions {limit:1000, usage} reply, 1,001 rows of 20 KB plus one 16 MiB row | refused (37,190,897 bytes) | ≤ 14 MiB page with nextCursor | listed instead of refused | server/list_fit_tests.rs |
_acpmux/info of a chat with a 16 MiB title | refused (16,779,433 bytes) | under 15 MiB, title as a 1 KiB preview | answered instead of refused | server/list_fit_tests.rs |
Agent session: agent session
🤖 Generated with Claude Code