acpmux: a bare session name or id prefix reaches the one live session on any host; several are refused with each HOST/NAME command

Merged
TE
6d ago
+632-30

Leo, 2026-10-30: "i feel like there's still a big mess of uncategorized stuff in the sidebar + various not as ergonomic/one-command-y as i'd liike set of commands for interacting with stuff" / "good intuitive stuff" / "and also docs!"

Part of HQ#5197 (productionize team tools: one command per common task). No HQ issue covers acpmux name resolution itself.

What was wrong

Which session a bare name reached depended on the command:

  • send, last, wait, info, attach and rename went through _acpmux/info and the send queue. They took a session on this host first, so review reached the local review and silently ignored a peer's live review. A peer's name worked only when no local session matched (Hub::resolve, then resolve_remote).
  • retry and session cancel|stop|resume went through _acpmux/resolve, which refused that same case.
  • A closed local fixer beat a live linux-b/fixer. A closed and a live deploy on two peers counted as ambiguous, so the command answered no_session.
  • The ambiguity refusal listed qualified ids, not a command you could run.

What changes

  • One rule, in the daemon (hub/key_route.rs): a bare key (no /, not an exact local id) names the one session that has it as its whole name or an id prefix, across this daemon and every peer it lists. Name and id are one tier, so a name never hides another session's id. Only when nothing matches that way does a name prefix name a session on this host (the daemon's existing rule). A peer's name prefix never matches, as resolve_remote never matched one: a short key never reaches another machine's chat.
    A sub-session's whole name (parent/child) is routed the same way, by whole name only. A key whose first segment is a peer, this machine or local stays exact (HOST/NAME, qualified ids).
  • Closed sessions count only when no live one matches. So session info done still reads a finished lane.
  • Several live matches are refused, never picked. A session here no longer wins over a peer's. Exit 2, session_ambiguous. --json carries error.data.choices with each choice's host, name, route and the key to use, every one of them (the message names 10). The CLI prints the exact command for each:
    $ acpmux send review "ship it"
    acpmux: usage: "review" names 2 live sessions: mac-a/review, linux-b/review; name one of them
      acpmux send mac-a/review 'ship it'
      acpmux send linux-b/review 'ship it'
    
  • Where it applies: in handle_request, on the unix socket only, before any check reads the key. It covers every session method that peer_forward.rs would forward, plus the send queue's request.sessionId, so send (also --no-wait), last, wait, attach and session info|cancel|stop|close|rename|tag|… all find the same session. Exact keys (HOST/NAME, peer/id, a local id) are left alone, and so is keep's exact ephemeral id. Web and peer connections are unchanged.
  • control_id (retry, session cancel|stop|resume) uses the same live-over-closed rule and the same refusal.
  • Current rule kept: an exact id, HOST/NAME and a qualified id still win as they did before. Changed rule: local no longer wins on a bare name. Two resolvers disagreed here, and the brief asks never to pick silently.
  • Docs: README "lifecycle controls" section, the agent guide (skills/acpmux/SKILL.md, §6), and acpmux send --help with examples.

Related: #27791 (open, its lane is gone) adds sub-session short names (a child answering to its short name) in Hub::resolve. The two compose: this router passes over a key that no whole name or id prefix matches, so the short-name lookup there still runs after it. Whichever lands second takes a small text merge in cli/errors.rs / server/requests.rs.

Tests

  • hub::key_route::tests
    • a_bare_name_only_a_peer_has_routes_to_that_peer
    • a_name_on_this_machine_and_a_peer_is_refused_never_picked
    • an_id_prefix_names_a_peers_session_or_one_here (also: a shared prefix is refused, and a peer's name prefix is no route)
    • closed_sessions_do_not_count_while_a_live_one_matches
    • one_live_session_wins_over_closed_ones, several_live_sessions_are_refused_with_each_host_and_name
    • a_sub_sessions_whole_name_is_routed_across_hosts_too
  • server::key_route_tests (through the real dispatcher)
    • info_by_a_name_here_and_on_a_peer_is_refused_with_both
    • a_send_to_a_shared_name_is_refused_not_queued_for_the_one_here
    • a_sub_sessions_whole_name_here_and_on_a_peer_is_refused
  • cli::resolve::tests
    • an_ambiguous_name_prints_the_exact_command_for_each_host
    • resolve_cards_skip_closed_sessions_and_refuse_several_live_ones

Observed / Not run

  • Red on base (cmux-ci run 41012776600): with the base's src/hub, src/cli, server/requests.rs and server/peer_forward.rs and only the new request tests, cargo test -p acpmux --lib -- server::key_route_tests fails 3 of 3. info review returns the local session silently; send review is queued ("status":"queued","target":"review") for the local one; info review/child returns the local sub-session.
  • Green on head d95ba2f: the same 3 tests pass, plus the hub and CLI tests listed above. Full cargo test -p acpmux --lib at d95ba2f: 2017 passed, 0 failed (cmux-ci run 41014420558); tests/test_ci_fleet_rule_lints.py OK at 00419b6 (run 41013564700), and the crash ratchet names no acpmux row.
  • Not run: a live two-host check.

Changelog

Changed: acpmux commands take a bare session name or id prefix for a session on any peer. A name several live sessions share is refused with the exact HOST/NAME command for each.

risk: normal (CLI and daemon session-key routing, local socket only).

Agent session: agent session

🤖 Generated with Claude Code