acpmux: a turn the person did not start never pings, and says whether it is quiet

Merged
TE
4d ago
+456-2

Leo, about a chat message that said only "Nothing new for you yet.": "why do you guys keep sending me this/get pinged to send this". Offered "a turn you didn't start that has nothing new to tell you shouldn't show up as a message or ping you", he said "yeah".

What happens today

Claude Code runs turns acpmux never prompted: when a background task or subagent ends (a task notification), on a scheduled wakeup or /loop tick, or for a hook. The agent answers, often with a no-op line. In a real coordinator chat on cosmic-gray, 115 such turns ran against 13 prompted ones; "Nothing new for you yet." was one of them. acpmux logged their words as ordinary rows, and each one became the chat's preview / lastMessage, which is the sidebar line and the body of the next "finished" notification.

What changes (acpmux, Rust)

New hub/background_turn.rs. A system/init that comes while no acpmux turn runs, followed by model work (a message, thought, tool call or plan), is a background turn. acpmux brackets it with two mux background_turn records:

  • {state: "started", id, trigger}, where trigger is task_notification, scheduled (a command_lifecycle start) or system (anything else, such as a hook);
  • {state: "ended", id, trigger, quiet, reason}, where reason is result, prompt (a prompt started an acpmux turn while it ran, and the rest belongs to that turn) or exit.

Such a turn never pings:

  • it never marks the chat unread;
  • it never ends an acpmux turn, so lastTurn and the cmux-tui-core notices built from it don't move. This was already true before this PR, and the test now pins it;
  • new: its words stay off preview / lastMessage, so the sidebar line and notification bodies keep the person's last real reply.

The rule for "worth seeing", decided here only: quiet is true when the reply is at most one short line (160 characters) with no question mark and no link (://). The webview draws a quiet turn as one muted line that opens on a click, and draws any other background turn in full. Neither kind pings. Turns the person started, and inbox messages from other agents (which are acpmux prompts), behave exactly as before.

A system/init with no work after it starts nothing, so Claude Code's init at startup is not a turn. The record is documented in docs/acpmux-schema.json.

Pairs with #30205 (the webview, which draws these records). Older logs have no records and render as before.

Tests

  • Unit tests (hub::background_turn::tests, 6): the quiet rule; the bracket and trigger for a task notification, a scheduled wakeup and the system case; an acpmux turn is never a background turn; an init with no work is no turn; a prompt ends a running one.
  • Integration tests (hub_integration background_turns, 2), using a new own-turn: FIFO :: TEXT scenario in fake_claude.py that runs Claude Code's own turn for a task notification while the chat is idle and hidden:
    • "Nothing new for you yet." gives one quiet bracketed turn. unread stays false, lastTurn is unchanged and lastMessage is unchanged.
    • A two-line reply is not quiet and still never pings.
  • Fleet runs: all passed on linux-pool job b2b4cd507a5aab68e0e63b40; clippy --all-targets printed no warnings.

Risk: the tap now logs one extra mux record per background turn and checks one small mutex per own update. The hold, turn lock and unread paths are untouched.

Agent session: agent session

šŸ¤– 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

acpmux now brackets turns Claude Code runs on its own — when a background task or subagent ends, on a scheduled wakeup or /loop tick, or in a hook — with mux background_turn records, and keeps their words off the chat's preview and lastMessage. Previously such a turn's reply became the sidebar line and the next notification's body, pinging the person even when it said nothing new. These turns also never mark the chat unread or move lastTurn.

  • A turn is quiet when its reply is one short line (≤160 characters) with no question mark and no link. The webview draws a quiet turn as one muted line that opens on a click, and any other background turn in full; neither kind pings.
  • The started record carries the trigger (task_notification, scheduled, or system); the ended record adds quiet and reason (result, prompt, or exit). The schema in acpmux-schema.json documents both.
  • A system/init with no model work after it starts no turn, so Claude Code's startup init is unaffected. Turns the person started and inbox messages from other agents behave as before, and older logs render as before.
  • Adds unit tests for the quiet rule and bracketing, plus integration tests that run Claude Code's own turn over a FIFO and verify the chat stays unread with lastTurn and lastMessage unchanged.