acpmux: a chat keeps its background tasks while a restarted daemon adopts its agent

Merged
TE
5d ago
+107-4

Summary

After an acpmux update restart, a chat's own agent could not read or stop its background tasks for the first 17-26 s. bg_list and bg_output answered "a chat reaches only its own background tasks" while the tasks kept running. On 2026-11-01, two lanes on one lane VM lost their review and CI tasks this way: one at 04:15Z, the other at about 04:32Z. That VM restarted its daemon 10 times in 90 minutes, at 03:36, 03:46, 04:15:11, 04:21, 04:29:17, 04:34:22 and later.

Cause

Of the three suspects, the culprit is (a): the agent's MCP connection reconnects to a daemon run that does not yet know its token. (b) and (c) are not the cause. The task folder is keyed by the chat's id, and the id does not change.

  • The agent's acpmux MCP server (secret_mcp::serve) opens a new daemon connection for each tool call. Each new connection proves its chat with the caller token in the harness environment. That token is an HMAC under the secret of the daemon run that started the harness.
  • A restarted daemon accepts an earlier run's token only after it adopts that chat's agent host: adopt_caller_token reads the token from the host record. Startup adoption runs after the socket is already serving. On that VM, with about 90 hosts, the gap from acpmux ready to startup complete was 17-26 s on every restart in the log.
  • An MCP call in that gap was bound as an unproven caller (Conn::bind_person), so conn.caller() != Some(session.id) at server/bg_tasks.rs:42. The same call works again once adoption is done. I confirmed this on the VM: the token those lanes' MCP servers still hold, which was issued at 22:22 on Oct 31, proves the chat against today's daemon.

Fix

A first unix-socket initialize that names a caller session but carries a token that proves nothing yet now waits for this run's adoption (adoption_ready) before its caller is bound (Hub::settle_caller). The order of events is:

  • Outside a restart window, adoption_ready is already true, so nothing waits.
  • After adoption, the token proves exactly as before. A token that adoption does not know is still refused.

No access is widened:

  • The same caller_proves / adopted_caller_proves checks decide.
  • Another chat's token is still refused.
  • A connection that is not local is not affected.

Tests

tests/durable_agents/bg_caller_restart.rs, a_chats_background_task_stays_its_own_while_a_restarted_daemon_adopts_its_agent:

  1. A real daemon starts a bg task through the agent's caller token.
  2. The daemon restarts, and ACPMUX_TEST_ADOPT_HOLD_MS=3000 holds adoption open the way a large host count does.
  3. The test checks that host_adopted has not been logged. Then a fresh connection carrying the earlier run's token lists the task and reads its output. This covers (i) the daemon restart and (ii) the MCP reconnect.
  4. In the same window, all three of these are refused: another chat's token for this chat, this token for another chat, and a junk token.
  5. After acpmux session restart (iii, a new harness process with this run's token), the chat still lists the task and stops it.

Red/green (cmux-ci test -- cargo test -p acpmux --test durable_agents bg_caller_restart): results are in the comments below, red at the test commit and green at the head.

Observed / Not run

  • Observed: the daemon journal on the lane VM (restart times, the adoption window), host records and process environments for both chats, and a live probe that their tokens prove the chat on the current daemon run.
  • Not run locally: no cargo build or test on the lane VM. All tests ran through cmux-ci.

Changelog

Fixed: right after an acpmux update, an agent can still read and stop its own background tasks.

risk: access control (caller binding). The PR changes only when the binding is decided, never what proves a caller.

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

Fixes acpmux so a chat keeps reading and stopping its own background tasks right after a daemon restart, when its agent reconnects with the previous run's caller token while the daemon is still adopting hosts. Previously, bg_list and bg_output refused those calls for 17–26 s on a lane VM with ~90 hosts, which cost two lanes their review and CI tasks on 2026-11-01.

  • A first unix-socket initialize whose token proves nothing yet waits for this run's adoption (Hub::settle_caller) before binding the caller; outside a restart window nothing waits, and a token adoption never knows is still refused.
  • Adds a regression test that starts a real background task, restarts the daemon with adoption held open, and verifies the chat still lists and reads the task while another chat's token, this token for another chat, and a junk token are all refused, and that a fresh agent process still owns and stops the task.