Summary
Cross-node terminal attach fails with HTTP 404 agent_not_found for 9 of the 10 agents running on sf-mini, and for 1 of 9 on finn-mini. The node is healthy, the agents are alive, and the node truthfully advertises them every 12s.
The failure is not in the node's terminal subsystem. It is a divergence between two independent in-process sources of truth inside the same broker for "which agents am I running", only one of which the control plane uses to authorize attach.
This is not an sf-mini-specific outage. sf-mini is the same fleet-wide defect at 90% instead of 11%.
Measured drift (2026-08-23 ~10:40Z)
| node |
ver |
heartbeat active_agents |
relay:live-agents:v1 names |
roster rows active on node |
drift |
| sf-mini |
11.8.1 |
10 |
10 |
1 |
9 |
| finn-mini |
11.8.1 |
9 |
9 |
8 |
1 |
| cloud-3129-sched-code |
11.8.2 |
1 |
1 |
1 |
0 |
Every agent in the "drift" column is unattachable. Every one not in it attaches fine.
Reproduction (with control)
$ agent-relay node agent attach sandbox-lead-0822 --node sf-mini --mode view
Error: The terminal-session request was rejected. Upstream message
"No active agent named 'sandbox-lead-0822' on node 'sf-mini'".
endpoint "https://cast.agentrelay.com/v1/nodes/sf-mini/terminal/sessions";
HTTP 404; code agent_not_found; attempts 1 (not retried because the failure was terminal).
$ agent-relay node agent attach branch-triage-0822 --node finn-mini --mode view
{"kind":"worker_stream","name":"branch-triage-0822",...} # succeeds, live screen
agent-relay fleet agent list already labels the defect — it is the only surface that shows both sides:
sf-mini sandbox-lead-0822 remote live <- node says live, roster does not bind it
finn-mini branch-triage-0822 remote live+roster <- both agree
Root cause
1. Attach authorizes off the roster, which the node cannot repair.
cloud/packages/relaycast/src/fleet/routes.ts:355 gates session creation on a five-predicate row lookup:
SELECT id FROM agents
WHERE workspace_id = ? AND name = ? AND status = 'active'
AND location_type = 'via_node' AND location_node_id = ? LIMIT 1
Node liveness (findTerminalAttachNode, isLive) passes before this, and the terminal-transport check (terminal_connected) runs after it — so a node with a perfectly healthy terminal transport never gets that far. Two different predicates fail in the wild, both surfacing as the identical 404:
status = 'active' — 9/10 sf-mini agents are offline in the roster (sandbox-lead-0822: last_seen 2026-08-23T08:53:14Z, frozen ~1h45m).
location_node_id = ? — abx-protocol-review is roster-active but bound to cloud-3061-repro-0817, while both sf-mini and finn-mini advertise it live. Attach on sf-mini 404s for this one too, via the other predicate.
2. The two sources of truth.
In relay/crates/broker/src/node_control.rs the broker maintains two independent sets:
load.active_agent_names → rebuilt into the relay:live-agents:v1 capability on every heartbeat, 12s (heartbeat(), ~line 503).
inventory → sent as inventory.sync on connect, on UpdateInventory, and on a 60s inventory_refresh tick (INVENTORY_REFRESH_INTERVAL, line 34).
Only inventory.sync writes the roster. reconcileNodeInventory (cloud/.../durable-objects/node.ts:330) sets status='active' for names in the payload, and at line 359 actively sweeps to offline any roster-active agent bound to the node that is absent from the payload.
3. Why it is permanent, not transient.
The reclaim guard admits any agent that is not currently active:
AND ( location_node_id = ? OR status != 'active' OR (location_type='via_node' AND location_node_id IS NULL) )
sandbox-lead-0822 is offline, so status != 'active' is TRUE — the very next sync should reclaim it. sf-mini has sent ~100 inventory.syncs since 08:53 and it is still offline.
⟹ sf-mini's inventory vector does not contain these agents, while its active_agent_names does. The sync is running and omitting them. Something (a reconnect, or a broker restart that re-adopted the PTYs) repopulated active_agent_names without repopulating inventory; the 08:53 sweep then marked all of them offline and every 60s sync since has re-confirmed their absence.
relay:live-agents:v1 — the one signal that is correct — has zero consumers in the control plane. It exists only in relay/packages/cli/src/cli/lib/fleet-live-agents.ts. grep across cloud/packages/relaycast returns nothing. The node can shout the truth every 12s forever and attach will never hear it.
Why this also explains #1593
#1593 reports DMs silently dropped for agents alive 1h15m–5h, every send returning recipientMatched: true. That is the same roster row. Delivery routes on location_*/status; attach authorizes on location_*/status. One stale row breaks both, which is why the symptoms share an age threshold and why pending_messages is 0 — the route is resolved against a row that no longer points anywhere live. I believe these are one bug, not two.
Proposed fix
Immediate (control plane, unblocks 10 stranded agents): treat relay:live-agents:v1 as authoritative for attach. In handleCreateTerminalSession, when the roster lookup misses, fall back to the node's heartbeat live-agent set before returning 404 — the node is the ground truth for its own processes. Attach is already gated on a workspace key and node liveness; this adds no authorization surface.
Correct (broker): collapse the two sets. active_agent_names and inventory should be one structure, or inventory should be derived from the live PTY set at send time rather than accumulated by command. As long as they are independently maintained they will diverge again.
Defensive (control plane): the line-359 sweep should not mark an agent offline while the same node's heartbeat still advertises it as live. Right now inventory.sync can un-person an agent that the heartbeat says is running.
Observability: fleet agent list already computes the drift. Nothing alerts on it. A node whose live_names − roster_active > 0 is silently accumulating unmanageable agents.
Recovery of the 10 stranded agents — needs a decision
UpdateInventory triggers an immediate resync, and spawning any agent on sf-mini issues one. If the broker's inventory vector still holds the 10 agents, a spawn would reclaim all of them at once and restore attach. If it does not, they stay offline and we have learned the vector is genuinely empty.
That is a cheap, non-destructive experiment (spawn, not restart, not release) — but it mutates a node holding 10 agents with possibly-unpushed work, so I am not running it without a go-ahead. The alternative is a control-plane row correction, which is a write to agents and also needs sign-off.
Environment
- Observed from
chief-broker (relay-broker/11.8.0), CLI 11.8.1, gh khaliqgant.
- Note: chief-broker at 11.8.0 does not publish
relay:live-agents:v1 at all — nor do the 11.6.9 nodes. fleet agent list degrades those to count only (degraded) / "inventory unavailable", so drift there is not even measurable.
Summary
Cross-node terminal attach fails with HTTP 404
agent_not_foundfor 9 of the 10 agents running onsf-mini, and for 1 of 9 onfinn-mini. The node is healthy, the agents are alive, and the node truthfully advertises them every 12s.The failure is not in the node's terminal subsystem. It is a divergence between two independent in-process sources of truth inside the same broker for "which agents am I running", only one of which the control plane uses to authorize attach.
This is not an sf-mini-specific outage. sf-mini is the same fleet-wide defect at 90% instead of 11%.
Measured drift (2026-08-23 ~10:40Z)
active_agentsrelay:live-agents:v1namesactiveon nodeEvery agent in the "drift" column is unattachable. Every one not in it attaches fine.
Reproduction (with control)
agent-relay fleet agent listalready labels the defect — it is the only surface that shows both sides:Root cause
1. Attach authorizes off the roster, which the node cannot repair.
cloud/packages/relaycast/src/fleet/routes.ts:355gates session creation on a five-predicate row lookup:Node liveness (
findTerminalAttachNode,isLive) passes before this, and the terminal-transport check (terminal_connected) runs after it — so a node with a perfectly healthy terminal transport never gets that far. Two different predicates fail in the wild, both surfacing as the identical 404:status = 'active'— 9/10 sf-mini agents areofflinein the roster (sandbox-lead-0822:last_seen2026-08-23T08:53:14Z, frozen ~1h45m).location_node_id = ?—abx-protocol-reviewis roster-activebut bound tocloud-3061-repro-0817, while both sf-mini and finn-mini advertise it live. Attach on sf-mini 404s for this one too, via the other predicate.2. The two sources of truth.
In
relay/crates/broker/src/node_control.rsthe broker maintains two independent sets:load.active_agent_names→ rebuilt into therelay:live-agents:v1capability on every heartbeat, 12s (heartbeat(), ~line 503).inventory→ sent asinventory.syncon connect, onUpdateInventory, and on a 60sinventory_refreshtick (INVENTORY_REFRESH_INTERVAL, line 34).Only
inventory.syncwrites the roster.reconcileNodeInventory(cloud/.../durable-objects/node.ts:330) setsstatus='active'for names in the payload, and at line 359 actively sweeps toofflineany roster-active agent bound to the node that is absent from the payload.3. Why it is permanent, not transient.
The reclaim guard admits any agent that is not currently active:
sandbox-lead-0822isoffline, sostatus != 'active'is TRUE — the very next sync should reclaim it. sf-mini has sent ~100 inventory.syncs since 08:53 and it is still offline.⟹ sf-mini's
inventoryvector does not contain these agents, while itsactive_agent_namesdoes. The sync is running and omitting them. Something (a reconnect, or a broker restart that re-adopted the PTYs) repopulatedactive_agent_nameswithout repopulatinginventory; the 08:53 sweep then marked all of them offline and every 60s sync since has re-confirmed their absence.relay:live-agents:v1— the one signal that is correct — has zero consumers in the control plane. It exists only inrelay/packages/cli/src/cli/lib/fleet-live-agents.ts.grepacrosscloud/packages/relaycastreturns nothing. The node can shout the truth every 12s forever and attach will never hear it.Why this also explains #1593
#1593 reports DMs silently dropped for agents alive 1h15m–5h, every send returning
recipientMatched: true. That is the same roster row. Delivery routes onlocation_*/status; attach authorizes onlocation_*/status. One stale row breaks both, which is why the symptoms share an age threshold and whypending_messagesis 0 — the route is resolved against a row that no longer points anywhere live. I believe these are one bug, not two.Proposed fix
Immediate (control plane, unblocks 10 stranded agents): treat
relay:live-agents:v1as authoritative for attach. InhandleCreateTerminalSession, when the roster lookup misses, fall back to the node's heartbeat live-agent set before returning 404 — the node is the ground truth for its own processes. Attach is already gated on a workspace key and node liveness; this adds no authorization surface.Correct (broker): collapse the two sets.
active_agent_namesandinventoryshould be one structure, orinventoryshould be derived from the live PTY set at send time rather than accumulated by command. As long as they are independently maintained they will diverge again.Defensive (control plane): the line-359 sweep should not mark an agent offline while the same node's heartbeat still advertises it as live. Right now inventory.sync can un-person an agent that the heartbeat says is running.
Observability:
fleet agent listalready computes the drift. Nothing alerts on it. A node whoselive_names − roster_active > 0is silently accumulating unmanageable agents.Recovery of the 10 stranded agents — needs a decision
UpdateInventorytriggers an immediate resync, and spawning any agent on sf-mini issues one. If the broker'sinventoryvector still holds the 10 agents, a spawn would reclaim all of them at once and restore attach. If it does not, they stay offline and we have learned the vector is genuinely empty.That is a cheap, non-destructive experiment (spawn, not restart, not release) — but it mutates a node holding 10 agents with possibly-unpushed work, so I am not running it without a go-ahead. The alternative is a control-plane row correction, which is a write to
agentsand also needs sign-off.Environment
chief-broker(relay-broker/11.8.0), CLI 11.8.1, ghkhaliqgant.relay:live-agents:v1at all — nor do the 11.6.9 nodes.fleet agent listdegrades those tocount only (degraded)/ "inventory unavailable", so drift there is not even measurable.