Skip to content

Cross-node attach 404s node-wide: broker heartbeat and inventory.sync disagree on which agents are running (roster drift; 9/10 agents on sf-mini unattachable) #1602

Description

@khaliqgant

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions