acpmux: carried turns arm their own cap, hold their own output and count lane reports
Follow-ups from the independent review of #27536 ("a turn ends when Claude Code ends it"). Its SHIP receipt listed four non-blocking notes. This PR fixes three of them; the fourth turns out to need no code change (see below).
#27536 fixed Leo's asks: "what should we do?", "should i steer", and "did ANY of the reserach/think tank/pls go look into this sessions finish???".
What changes
- Every carried turn arms its own turn cap. On base, only the first carried turn (
start_carried) armed the #27520 timer. The turnsrun_carriedstarted after it, and the carried turns a restart adopts (recover_carried), got none. So an agent message queued behind them never got the cap notice. Nowbegin_carriedarms the timer for every carried turn. A turn a restart adopts (a carried one, or a prompt's own inhosts.rsrecover_work) arms it too. The timer now counts from the turn's ownstarted_at, so an adopted turn's cap doesn't start over. - Each carry holds Claude's output once, with its own bound. On base, the hold started only when the count of owed carried turns went from 0 to 1. A second carry made before the first one's prompt came (that hold's 10 s bound already passed) got no hold, so its turn's words were read before that turn started.
Translator::holdingnow returns the newest carry's number. The in-process reader (agent.rs) and the agent host (host.rsread_held/holds) hold once per carry. - A carried turn made only of lane reports is marked report-only.
begin_carriednever calledlane_turn_started, so a coordinator's carried turn on its lanes' reports reported upward. Reports never steer on their own: they wait in the queue (lane_deliver). So this happens only when the person presses Send now on a queued report and Claude Code hasn't read it by its end_turn. Each steer now records whether it is a lane report (Continues.reports). A carried turn counts as report-only when every steer it carried is a report (lane_turn_on). A carried turn a restart cuts before itsturn_startedkeeps the mark (carriedTo.reportsOnly). One that had already started comes back without it, but nothing depends on that: lane links live only in memory and aren't restored, so after a restart no turn of that session reports upward anyway.
Note 2, "an unread turn-cap notice can be carried into the next prompt", is real but needs no code change. The notice line is already in Claude Code's queue when its turn ends. Claude Code takes it as soon as it emits the result, so cancel_async_message would lose that race, and neither the adapter nor the hub can pull it back. All that happens is one short carried turn on the notice ("finish the current step and end your turn"), which ends as the notice asks. The held digest goes next, and the carried turn keeps a cap of its own (turn_cap_noticed still names the turn the notice was for).
Tests (red, then green)
Red commits come first:
tests/hub_integration/carried_turns.rsa_carried_turn_after_a_carried_turn_has_its_own_cap(behavioral).tests/agent_host/carried_hold.rsa_second_carry_holds_claude_output_again(behavioral, about 11 s: it waits out the first hold's bound).src/hub/carried_turn.rsa_carried_turn_on_lane_reports_only_reports_nothing_upward. Base has no way to mark a steer as a report, so this red commit doesn't compile on base.src/hub/turn_cap_tests.rsan_adopted_turns_timer_counts_from_the_turns_startwas added with the fix.
Observed / Not run
Ran through cmux-ci test (Blacksmith Linux, 32 vCPU):
- Red at
d07170120b8(red tests on base), run 40993825537:cargo test -p acpmux --test hub_integration a_carried_turn_after_a_carried_turn_has_its_own_capfailed:the third turn's cap sent its notice,left: [].cargo test -p acpmux --test agent_host a_second_carry_holds_claude_output_againfailed:the second carry held nothing(read: LATE-2was read before its prompt came).- The report-only unit test doesn't compile on base (see above).
- Green at
e222db5536b, run 40994153187:cargo clippy -p acpmux --all-targets -- -D warningsis clean.- lib filters
carried_turn(12),turn_cap(12),carry(18) andlane_report(27) all pass. --test hub_integration carried_turns(10),--test agent_host carried_hold(2),--test lane_reports(17) and--test durable_agents carried(1) all pass.
Not run: the full acpmux suite (left to PR CI), and a live Claude Code session.
Risk
risk:normal. acpmux daemon only, Claude carried turns. The turn cap is off by default (turnCapMs). The hold change keeps the same 10 s bound per carry.
Changelog
none
Agent session: agent session
š¤ Generated with Claude Code