acpmux: a chat's inbox needs no Labs team messaging; a chat that cannot read one has none
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 listsinbox_read. Every one created after does. Those older agents have onlyteam_inbox/team_ack, the Labs team mailbox. acpmux inboxis the team mailbox too. A Codex helper started after #27686 ranacpmux inbox --jsonbecause 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,/readand/ackfrom an agent proven to run in a chat here now answer with that chat's inbox, in the mailbox's page shape (server/team.rschat_inbox,hub/inbox_team_page.rs). Each message comes whole as itsbody, under its prompt id. Reading by a thread id gives the whole thread. A read or an ack marks messages read, asinbox_readdoes. A page marks nothing, as the mailbox's doesn't, andacpmux inboxprints only each message's first line. This reaches the stuck agents without restarting them: their oldteam_inboxandacpmux inboxnow 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.rsreads_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
acpmuxserver. 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: noACPMUX_AGENT_TOOLS=0, no--strict-mcp-configfor Claude Code, andACPMUX_AGENT_TOOL_SERVERSkeepsacpmux.
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). - The chat's tool rows, which each launch writes and which outlast a restart, show an enabled
Tests (committed red before the fix)
| Test | Before 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 whole | green |
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:331 | green Ć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_inbox14 of 14 passed three times. In fullhub_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_homeandstore::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) andsession_scope::tests::a_real_user_manager_takes_the_slice_limits_once(it passed on rerun at head and on base).
- Also red on base:
- Observed:
cargo clippy -p acpmux --all-targets -- -D warningsis clean. The panic ratchet's clippy (-p acpmuxwith 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.pyreports 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-tuitogether, sincecmux-tuineeds 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, callingteam_inboxafter the update. The integration test drives the same_acpmux/team/inboxrequest 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