feat(acpmux): byte-bound background-process history with opt-in pages (SW-11)
SW-11: opt-in, byte-bounded background-process history pages
Root cause
_acpmux/process/list still builds, clones, stats output files for, and JSON-encodes every retained background process in a session or globally, then may receive message.too_large at the 15 MiB JSON-RPC framing limit. An agent with thousands of completed tasks can therefore fail to list its processes after paying for all their allocations and metadata reads. HQ process-history audit.
Change
- Existing
_acpmux/process/list {sessionId?}behavior and response remain exactly as before for current clients. - A caller opting into
{sessionId, limit, cursor?}gets{processes, nextCursor}. The limit is a positive integer capped at 1,000; a page is cut again by a 15 MiB minus 64 KiB byte budget, so large process commands cannot overflow a reply. A cursor requires a limit, and paging requires an explicit session ID. - A cursor identifies the last process's durable
idin that session, looked up with the existing #29277 process-id index. Rows remain in append order across status updates and additional processes; an unknown/stale cursor is refused explicitly. - Clone at most the requested window under the session mutex, then resolve output-file lengths and serialize the page after unlocking (preserving the #29238 guarantee). Stop serialization once the byte budget is filled;
nextCursornames the last emitted record. - A single oversized command/detail produces a small
omitted: {reason:"too_large", bytes}row with itsid,sessionId,toolCallId,taskId, harness, state and timestamps, retaining the identifiers required by Stop and the UI. If the identity itself cannot fit, return an ordinarymessage.too_largeerror while keeping the connection. - The docs describe the opt-in wire format. The existing protocol spec records this method's params/result as open JSON (
true), so no generated type migration is needed.
Regression tests
New processes_page_tests.rs builds 3,000 recorded tasks, follows every page at a constrained byte budget, proves all IDs returned exactly once and output size under budget; checks a live status update plus an appended task between pages, unknown cursor refusal, and oversized-command stub semantics. New server/processes.rs tests verify absent/positive/capped limit and malformed/bare cursor errors.
Performance
No perf numbers: source limits cloning and metadata reads to a requested per-session page instead of all historical rows and stops serialization at the byte budget. The 3,000-row regression proves page coverage and bounded encoded responses; no same-machine CPU, RSS or p95 request latency comparison has run.
Validation and guardrails
- Base
feat-cmux-next@42d132cfafad6936f1ac7ba85cf5fb028ef80d68(after #29238/#29277 merged). - Four files (daemon hub+handler, tests, docs); no change to unpaged clients, process event streams, running process bookkeeping, auth (the local-only check stays ahead of the handler) or tool behavior.
- Draft / unrun locally: run
cd cmux-tui && cargo test -p acpmux --lib process_page_tests && cargo test -p acpmux --lib process_page_params_tests,cargo test -p acpmux --test hub_integration -- process,cargo fmt --check, Clippy and NEXT daemon protocol tests. Run 1/100/3000-page comparisons, including a 16-MiB record and concurrent live updates, on the same host before approval. - Scope limit: paging currently requires a
sessionId; global listing retains its old format and limitation. A subsequent client/UI migration can opt into paging and retire browser-held full histories once cursor/delta reconciliation is available. - Do not merge until API owner and process-registry owner validate the compatibility, oversized-row contract and security boundary.
Capture
No capture: internal daemon and JSON-RPC paging work; UI remains unchanged.
Changelog: Local clients can page a chat's background-process history in byte-bounded windows while retaining the original full-list behavior for existing callers.
SW-11 full ongoing findings.
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds opt-in, byte-bounded paging to _acpmux/process/list so a session with thousands of background processes can list its history without exceeding the JSON-RPC frame limit. Existing callers keep the exact same full-list behavior and response, and the opt-in wire format is documented in the schema and protocol docs.
Paging contract
- Passing
{sessionId, limit}with an optionalcursorreturns{processes, nextCursor}instead of the full list;limitis a positive integer capped at 1,000, and pages are cut at a byte budget just under the message frame limit. cursoris the last received process id; it requires alimit, paging requires a session ID, and an unknown cursor is refused explicitly.- Only the requested window is cloned under the session mutex, then output-file lengths are resolved and the page is serialized after unlocking; a single oversized record becomes an identity-preserving
omittedstub. - A running-process index tracks live slots in append order, making idle checks O(1) without scanning completed history.
- Regression tests page 3,000 recorded tasks, prove every id returns exactly once within the byte budget, and cover live status updates, appended tasks, unknown cursors, and oversized commands; the duplicate running-process test module was removed.