Severity: medium/major · packages/engine/src/engine/node.ts:768-797 (reserveNodeAgentSlot/releaseNodeAgentSlots) vs engine/nodeProvider.ts:309-333 (recomputeNodeAggregate)
Binding operations increment/decrement nodes.activeAgents directly to enforce capacity (activeAgents = activeAgents + 1 guarded on activeAgents < maxAgents), but every broker node.register/node.heartbeat overwrites the same column with the provider self-report sum:
const activeAgents = providers.reduce((sum, p) => sum + p.activeAgents, 0);
await db.update(nodes).set({ ...activeAgents, maxAgents, ... })
maxAgents is likewise recomputed while the binding path reads the persisted value. The two accounting schemes disagree and clobber each other.
Failure scenario: an agent registers via a node (slot reserved, activeAgents=1); before the broker's roster reflects it, a heartbeat reports active_agents=0 → recomputeNodeAggregate resets the column, dropping the reservation → a concurrent capacity check under-counts and over-subscribes the node. Conversely a stale high provider report wedges a node at false capacity. This is the same bug class documented as a past incident in relaycast-cloud's CLAUDE.md ("binding-only deactivation left nodes.active_agents overcounting").
Fix: pick a single source of truth for the column (derive from active agent_node_bindings rows, or make the heartbeat path reconcile instead of overwrite).
Found during a multi-agent reliability review of the relay tool/protocol.
Severity: medium/major ·
packages/engine/src/engine/node.ts:768-797(reserveNodeAgentSlot/releaseNodeAgentSlots) vsengine/nodeProvider.ts:309-333(recomputeNodeAggregate)Binding operations increment/decrement
nodes.activeAgentsdirectly to enforce capacity (activeAgents = activeAgents + 1guarded onactiveAgents < maxAgents), but every brokernode.register/node.heartbeatoverwrites the same column with the provider self-report sum:maxAgentsis likewise recomputed while the binding path reads the persisted value. The two accounting schemes disagree and clobber each other.Failure scenario: an agent registers via a node (slot reserved,
activeAgents=1); before the broker's roster reflects it, a heartbeat reportsactive_agents=0→recomputeNodeAggregateresets the column, dropping the reservation → a concurrent capacity check under-counts and over-subscribes the node. Conversely a stale high provider report wedges a node at false capacity. This is the same bug class documented as a past incident in relaycast-cloud's CLAUDE.md ("binding-only deactivation left nodes.active_agents overcounting").Fix: pick a single source of truth for the column (derive from active
agent_node_bindingsrows, or make the heartbeat path reconcile instead of overwrite).Found during a multi-agent reliability review of the relay tool/protocol.