acpmux: a chat's inbox needs no Labs team messaging; a chat that cannot read one has none

Merged
TE
5d ago
+472-3

Three helpers tonight got an [inbox] New messages ... in 1 thread notice whose preview cut a standing order from their owner at 120 characters. Each then tried the one inbox reader it had and was refused: "team messaging is disabled; enable meshRelay.teamMessaging in Labs". So all any of them saw of the order was its first 120 characters.

The per-chat inbox (#27686) itself was never gated, but the readers these agents had were:

  • Agents started before #27686 have no inbox_read. An agent's MCP tools are fixed when it starts, and a daemon update doesn't restart it. In this host's session logs, no session created before #27686 merged ever lists inbox_read. Every one created after does. Those older agents have only team_inbox / team_ack, the Labs team mailbox.
  • acpmux inbox is the team mailbox too. A Codex helper started after #27686 ran acpmux inbox --json because the name fits, and got the same refusal.

The fix

  • The team mailbox's readers read the chat's own inbox while Labs team messaging is off. _acpmux/team/inbox, /read and /ack from an agent proven to run in a chat here now answer with that chat's inbox, in the mailbox's page shape (server/team.rs chat_inbox, hub/inbox_team_page.rs). Each message comes whole as its body, under its prompt id. Reading by a thread id gives the whole thread. A read or an ack marks messages read, as inbox_read does. A page marks nothing, as the mailbox's doesn't, and acpmux inbox prints only each message's first line. This reaches the stuck agents without restarting them: their old team_inbox and acpmux inbox now return the full text. Unchanged: Labs on, the person's connection, and sends to team addresses.

  • A chat that can't read an inbox has none (inbox.rs reads_inbox). Its agent was started without cmux's tools: a remote origin, ACPMUX_AGENT_TOOLS=0 (in the profile or daemon-wide), or --strict-mcp-config. Another agent's message then queues for it as a plain prompt, as before #27686, so the agent gets every word.

  • A notice to an agent that has no reader carries each message whole (deliver_notice, whole_notice, Hub::notice_reader_has_tools). The choice is made as the notice goes, not when its messages came. Two things must hold, or the notice lists every new message whole, with its sender and thread:

    • The chat's tool rows, which each launch writes and which outlast a restart, show an enabled acpmux server. So an agent that a restart adopted counts as it was started, whatever its profile says now. No rows count as no reader.
    • The profile its next spawn takes still gives the tools. That means the harness profile's, its defaults' and its preset's env and args, expanded (${model}, ${cwd}, ${home}) as the spawn expands them, then the chat's own env: no ACPMUX_AGENT_TOOLS=0, no --strict-mcp-config for Claude Code, and ACPMUX_AGENT_TOOL_SERVERS keeps acpmux.

    Both read only memory: the rows and the config. None of spawn_profile_for's file work runs for a notice. So a folder profile, which only its file describes, counts as no reader, and its notices come whole. This covers messages the inbox took before the chat's agent respawned without tools, and an adopted tool-less agent. The routing check above can't see either case (router reviews at 9aec20b, 9fb05cae and a54c9a5d).

Tests (committed red before the fix)

TestBefore its fix (tests only)At head
agent_inbox::with_team_messaging_off_a_chat_reads_its_inbox_whole: Labs off, a blocker from another agent becomes a notice with a preview only. inbox_read, and the team mailbox's inbox, read and ack, return its full text to the chat's own agent and to no other chat's. The person's connection still gets the Labs refusal.red at a8058365: the chat's own agent gets the Labs refusal from _acpmux/team/inbox (agent_inbox.rs:644)green
agent_inbox::a_chat_whose_agent_has_no_tools_gets_its_messages_as_prompts: a chat started with ACPMUX_AGENT_TOOLS=0 gets another agent's message as its own prompt, with no inbox thread or notice.red at a8058365: the message went into the inbox ("inbox":true, agent_inbox.rs:689)green
agent_inbox::a_notice_to_an_agent_that_lost_its_tools_carries_each_message_whole: the chat's profile changes to ACPMUX_AGENT_TOOLS=0 while its agent still has the tools. A blocker goes into the inbox, and its notice carries the message whole.red Ɨ2 at dc9a94f5: no notice carried the message wholegreen
hub::inbox::tests::a_notice_reader_has_tools_only_by_its_last_launch_and_its_next_spawn: five chats loaded from a stored meta, as a restart adopts them, with launch unknown. Only the one whose rows show an enabled acpmux server, and whose next spawn keeps the tools, reads previews. The others get whole notices: launched without tools, with the server off, spawning with ACPMUX_AGENT_TOOLS=0, with a server list without acpmux, or with a ${model} switch the spawn expands to 0, and a folder profile.red Ɨ2 at 0184fa1f: inbox_tests.rs:308, true where false (the adopted tool-less agent); red Ɨ2 at 766a4494 (its two last cases): inbox_tests.rs:331green Ɨ2

Commands: cargo test -p acpmux --test hub_integration agent_inbox and cargo test -p acpmux --lib -- hub::inbox::tests::a_notice_reader_has_tools.

Observed / Not run

  • Observed, on this lane VM at 20dbf3fa (the head): agent_inbox 14 of 14 passed three times. In full hub_integration, 621 passed and 5 failed. In the lib, 2474 passed and 3 failed. Each failure was compared by name with base 35c02175:
    • Also red on base: session_git_state::{git_state_of_a_held_chat_follows_a_checkout_mid_session, git_state_a_restart_tells_no_branch_the_log_already_told, git_state_of_a_purged_chat_is_forgotten, git_state_restores_the_no_git_record_baseline_before_attach}, daemon::state_file_tests::managed_unit_start_avoids_a_symlinked_default_home and store::startup_tests::listing_open_chats_does_not_read_their_log_directories. They fail on this host's environment.
    • Flakes, each failing once: prompt_supersede::a_prompt_queued_before_a_restart_is_still_replaced (the base flake #28503 fixes; it passed at a54c9a5d) and session_scope::tests::a_real_user_manager_takes_the_slice_limits_once (it passed on rerun at head and on base).
  • Observed: cargo clippy -p acpmux --all-targets -- -D warnings is clean. The panic ratchet's clippy (-p acpmux with the ratchet's flags) finishes and finds no hit in the lines this PR adds. Its one hit in a changed file, inbox.rs:408, is old code outside the change. prepush-gate.py reports no new violations.
  • Independent review: gpt-6-astra (high, fast) gave BLOCK at 9aec20b, 9fb05cae and a54c9a5d. The third bullet above answers all three. It gave SHIP at the head, 20dbf3fa.
  • Not run: the ratchet over -p acpmux -p cmux-tui together, since cmux-tui needs the ghostty submodule this host doesn't build. Also not run: a durable agent adopted across a real daemon restart (the unit test loads the stored meta the same way), and a Codex agent on a real daemon, started before #27686, calling team_inbox after the update. The integration test drives the same _acpmux/team/inbox request over a proven agent connection.

The preview notice (render()) is unchanged here. Another lane is changing render() in the same file, so I sent it the shell form of the read for its new instruction text: acpmux session inbox "$ACPMUX_SESSION_ID" --thread ID --mark-read.

Changelog

Fixed: an agent can read its inbox messages in full when Labs team messaging is off, including agents started before the inbox existed; an agent without cmux's tools gets other agents' messages whole, as prompts or in the notice.

Agent session: agent session

šŸ¤– Generated with Claude Code