feat(acpmux): byte-bound background-process history with opt-in pages (SW-11)

Merged
TE
5d ago
+241-5

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 id in 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; nextCursor names the last emitted record.
  • A single oversized command/detail produces a small omitted: {reason:"too_large", bytes} row with its id, sessionId, toolCallId, taskId, harness, state and timestamps, retaining the identifiers required by Stop and the UI. If the identity itself cannot fit, return an ordinary message.too_large error 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 Autofix with [code]smith
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 optional cursor returns {processes, nextCursor} instead of the full list; limit is a positive integer capped at 1,000, and pages are cut at a byte budget just under the message frame limit.
  • cursor is the last received process id; it requires a limit, 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 omitted stub.
  • 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.