acpmux: a lane reports the end of each turn to the session that started it

Closed
TE
10d ago
+1360-4

A session that starts another one from inside itself now hears when each of that lane's turns ends, the way a Claude subagent's parent hears when it stops. The lane doesn't have to remember acpmux send. This PR covers lanes and parents on the same daemon. Two follow-ups build on it: A1b moves a lane to the session that gives it a later task, and A2 adds lanes and parents on a peer, persistence across restarts, and --report-to.

Leo-ask: ask-d3eb2bf9a5

"a lot of these sessions need to have more of the wherewithal of the subagents to be able to just report back more oten... we ahd a lot of situations with the inbox or whatever"

What changes

  • Who a lane reports to. The daemon keeps each lane's link to its parent. The lane's tag lane.reportsTo shows the parent's exact session id, but changing or removing the tag changes nothing, so no local client can silence a link.
    • It is set when a session runs session/new from inside another session, proven by its caller token. That is what acpmux new and acpmux run do from an agent's shell.
    • An ephemeral run (--ephemeral, a probe) gets no parent.
    • A lane keeps the parent that started it. A session that gives it a later task doesn't take it over yet; that comes in A1b.
  • The report. At each turn end the parent gets one prompt:
    [NAME] lane report: turn N ended, STATE.
    Session: ID
    Last message: the first 400 characters of its last reply
    Pull requests: URLs its gh pr create calls printed or the reply names, not reported before
    Commits named: full SHAs in the reply
    
    STATE is idle, working on its next prompt, waiting on a permission, waiting on a question: ..., or blocked: ... (a failed turn and the queue it paused, or a stopped agent).
  • How it is delivered. It goes in as a hub-set prompt with provenance {kind: "lane_report", from, fromName, to, turn, state}. No client can send that provenance.
    • It is treated as another agent's message (digest.rs): the person's prompts go first, and a busy parent takes several reports in one turn.
    • It never steers. A report waits for the parent's current turn to end, so it never becomes part of a turn that reports upward.
    • The sidebar line reads NAME: [NAME] lane report: ... (prompt_line.rs).
  • Limits.
    • At most one report per turn. Each lane gets one check at a time, and a turn end that comes while a check runs makes that check run again. One report per lane is in flight at a time, and a turn counts as reported only once the parent accepted the report. Several turns that ended meanwhile become one report of the last.
    • A turn whose agent couldn't start (it died and can't respawn) still reports, as blocked: ....
    • A report the same as the lane's last one (same state, reply and pull requests) is not sent again.
    • A turn that ran on lane reports only sends nothing upward. That includes a digest turn of several reports taken in together. Whether a turn was reports-only is read the moment it ends, so a turn queued behind it can't change the answer. A real task steered into a reports-only turn makes that turn report. Such a turn is known by its hub-set lane_report provenance, not by its prompt id, so no client prompt can pass for one (prompt_provenance.rs takes only agent_message from a client). So a coordinator's turn on its lanes' reports never reports to its own parent, and reports never answer reports.
    • At most 20 reports per lane in 10 minutes.
    • The prompt id is lane-report-<random uuid>, the same for every try of one turn's report and sent with resend, so a retry never runs twice and no other prompt can take its id.

The agent-pair guard

agent_pairs.rs counts a report as the lane's message to its parent: lane_report is one of the provenance kinds whose sender is a session here. A parent and lane that keep trading reports and answers with no work in between pause like any other pair. A report into a paused pair is held until the person resumes the pair; the hub treats the held refusal as accepted, so it never retries and never starts a turn the guard refused. The 20-per-10-minutes cap applies whatever the guard decides.

Review

Earlier versions were reviewed by Codex at xhigh and got BLOCK verdicts. Here is what this split does with those findings:

  • Fixed here:
    • The turn counts as reported only once the parent accepts the report.
    • One report is in flight at a time, and dedupe compares the full content.
    • Digest turns are suppressed.
    • Report turns are known by provenance, and report ids are random.
    • Reports never steer.
    • digest.rs recognises lane_report as an agent message, so a report the guard held still goes in as the lane's.
    • The ephemeral test keeps its connection open.
    • From the review of 56d783e4389 (BLOCK, five P2s), each with its own test:
      • a turn reported twice when two checks raced;
      • a queued turn making a reports-only turn report upward;
      • _acpmux/tag silencing a link;
      • a failed agent start never reporting;
      • a steered task in a reports-only turn being suppressed.
  • Moved to A1b, the retask follow-up, with its own tests: a queued retask losing a turn report, and the cap resetting on a retask.
  • Moved to A2, with its own tests: the peer guard, remote delivery, and restart.

This head's Codex xhigh review is still running. The PR opens before it finishes because the ask was overdue, and it merges only on SHIP.

Verification

Focused command: cargo test -p acpmux --test lane_reports (eleven lane_report_* tests). On the fleet I run it as:
cmux-ci run --class isolated --script scripts/ci/cmux-tui-rust-check.sh --ref SHA --arg test|all --arg lane_report_.

Observed / Not run

First round (huge-blue, with umask 022 and a private TMPDIR):

  • Red at 1797359a755a18873302ba1b793f1e11c6954917 (tests only): 5 of 6 fail with 0 of N reports in 20 s.
  • Green at 56d783e4389ce06a5e1844c58bc8909417453ecb: 6 of 6 pass.
  • Also green at that head:
    • hub_integration 369 of 370; the one failure is the known flaky test stop::a_cancel_ends_an_honouring_agents_turn_within_the_budget, which passes when run alone;
    • peer_integration 31, forum 21, ephemeral_run 4, cli_agent 5, --lib 563;
    • cargo clippy -p acpmux --all-targets -- -D warnings is clean.

Review-fix round:

  • Red at a9eabe643cad712cf5f99bcbd31c37483e360113 (new tests only, observed on huge-blue):
    • The four new tests for the tag, failed-start, steered-task and back-to-back cases fail with 0 of 1 reports in 20 s or 1 of 2 reports.
    • The queued-turn test passed there, since it depends on timing.
    • On huge-blue the back-to-back test was stopped by the pair guard pausing the pair, not by the bug. It now turns the guard off and is re-run red on the fleet (step 18eb415d46e8abbff2df33e1, still running).
  • Green at 3194e6fdb8085c464304bb1e1b8875c0a973a716: the fleet step 0f6729c327360d940c65d25f (fmt, clippy and tests filtered to lane_report_) is still running. I'll add its result here before merging. huge-blue's disk is at its 25 GB floor and cmux16's build guard refuses cargo below 15 GiB free, so this round runs on the fleet.
  • scripts/cmux-next/prepush-gate.py reports no new violations.

Crash ratchet: scripts/cmux-next/crash_ratchet.py reports rust acpmux: unwrap 179 -> 180, and base gives the same result, so that red is base's.

Not run:

  • Hosted runs 39717555063 and 39717656124 were for an earlier head and sat queued on stalled runners. I left them alone.
  • I haven't tried this live against a running daemon.

Size: 1,082 changed lines. Of those, 610 are code (hub/lane_reports.rs is 552) and 471 are tests, after five review findings each got a test. The 1,150-line version was already split three ways: this PR, A1b (retask) and A2 (peers, persistence, --report-to). This PR is the smallest part that works alone.

Risk: risk:normal. Lanes started inside other sessions gain a tag and send hub-set prompts. There's no new client method or param.

Changelog

Added: a session started from inside another acpmux session tells that session when each of its turns ends, with its last reply, state, pull requests and commits.

Agent session: huge-blue

šŸ¤– Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

A session started from inside another session now reports the end of each of its turns to the session that started it, the way a Claude subagent's parent hears when it stops, with no acpmux send needed. Each report names the turn, state, last reply, and any PRs or commits mentioned. Lanes and parents on the same daemon are covered; retasking, peers, persistence, and --report-to come in follow-up PRs.

Report delivery and limits

  • The daemon keeps each lane's link to its parent, set from the caller token at session/new; the lane.reportsTo tag only displays it and cannot change it.
  • Reports are hub-set prompts with a lane_report provenance no client can forge, and they never steer into a running turn.
  • A turn that ran on reports only sends nothing upward, so an agent's digest turn of reports never reports to its own parent.
  • At most 20 reports per lane per 10 minutes, one delivery in flight at a time, and a report identical to the last is not sent again.

Agent-pair guard

  • Lane reports count toward the agent-pair runaway guard; held reports are re-sent once the pair resumes.