You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A project workspace pin (<repo>/.agentworkforce/relay/workspace-key.json) that lacks an enrolledNodeId makes relay node upsilently ignore the fleet enrollment entirely. relay cloud enroll reports success, the node appears in the Cloud dashboard, and the node never heartbeats. Meanwhile relay fleet nodes run from that repo queries the pinned workspace, so it shows a completely different roster — with no error from either side.
Net user experience: the dashboard shows node A, fleet nodes shows node B, workspace active names a third answer, and nothing anywhere says the three are different workspaces.
Repro
cd<repo-with-a-workspace-key.json-pin># pin has no enrolledNodeId
relay cloud enroll --token 'ocl_node_enr_…' --enrollment-url 'https://agentrelay.com/cloud/api/v1/fleet/register' --name 'kjg-laptop'# → Enrolled node "kjg-laptop" (node_2108…) in workspace rw_7ccfea89.
relay node up --background
# → Broker started. Broker PID: 96942
relay fleet nodes
# → shows node "cloud", never "kjg-laptop"
Roster after the above — kjg-laptop exists and has never heartbeat:
kjg-laptop | node_210851746276208640 | offline | live False | hb None | created 2026-08-05T20:09:33Z
Broker log (~/Library/Logs/agentworkforce/relay/cloud.log.2026-08-05), same run:
20:09:36 INFO relay_broker::runtime::init: reusing cached node token
node_id=node_72241249b105bfd8b2edc3532abaec75 workspace_id=204337648549896192
workspace_id=204337648549896192 is the pinned auto-provisioned workspace — not rw_7ccfea89 where the enrollment lives.
Root cause
packages/cli/src/cli/commands/node.ts:112-131:
if(session?.enrolledNodeId){…}if(session){returnundefined;// node.ts:125 ← pin present, no enrolledNodeId → enrollment store never read}returndeps.resolveEnrollment({ … });// only reached when there is NO pin
fleet-enrollments.json is never consulted, no RELAY_NODE_TOKEN/RELAY_NODE_ID is set, and no warning is printed — the sibling branch at node.ts:147-149 does warn, this one doesn't. Locked in by node.test.ts:239-257.
There is also no workspace-id-vs-key reconciliation anywhere: the enrollment store holds workspace ids, the pin holds a key, so per the comment at node.ts:166-169 a mismatch is bypassed rather than detected. The broker then registers an auto-derived node in the pinned workspace (crates/broker/src/runtime/init.rs:305-330, :743-778), which is why it looks healthy.
Proposed fixes
node.ts:125 should warn() before returning — e.g. "project pinned to workspace <id>; ignoring N fleet enrollment(s). Run …". One line removes the entire class of confusion. Highest value / lowest risk.
relay cloud enroll should update the pin's enrolledNodeId (or refuse loudly when the repo is pinned to a different workspace). Today it writes only fleet-enrollments.json (commands/cloud.ts:920-927), so "enrolled into A while pinned to B" is a silently reachable state.
workspace switch|join should preserve enrolledNodeId — they call writeProjectWorkspaceKey(projectDataDir, workspaceKey) with no options (lib/workspace-session.ts:44-46), manufacturing exactly the broken state in fix fix(truncation): improvements #1.
Stop minting a throwaway workspace on every enrolled start. When an enrollment applies, resumePinnedProjectWorkspace bails because RELAY_NODE_TOKEN is set (lib/broker-lifecycle.ts:1327-1330), the broker auto-provisions a fresh workspace, and then broker-lifecycle.ts:1583-1586persists that throwaway key over the pin. Observed three separate free-plan workspaces all named relay-6bf605d7 created for one repo: 204337648549896192 (07-18), 210854606762164224, 210855543429939200. The node plane stays correct while the agent plane silently drifts to an empty workspace.
fleet status should match the roster by nodeId, not node name. It reported "no node named \"kjg-laptop\" in the workspace" while holding the correct nodeId in the same payload, and separately matched an unrelated stale node that happened to share the repo-basename name.
--workspace-key + enrollment is an unguarded footgun. It disables enrollment pickup (node.ts:170-175), so the intuitive "just point it at the right workspace" fix mints a duplicate node instead of serving the enrolled one. Warn when both an enrollment and an explicit key are present.
Env-var recovery path needs --broker-name or it loops. With RELAY_WORKSPACE_KEY+RELAY_NODE_TOKEN+RELAY_NODE_ID but no --broker-name, the broker takes the repo basename and tries to rename the enrolled node to it:
ERROR relay_broker::node_control: engine rejected a node control frame
code=node_name_conflict Node name "cloud" is already enrolled
ERROR relay_broker::node_control: … code=node_control_failed
Failed query: update "nodes" set "name" = ? where …
repeating indefinitely; heartbeats never land. Either derive the broker name from RELAY_NODE_ID or don't attempt the rename.
Working invocation (verified)
RELAY_WORKSPACE_KEY='<workspace key>' \
RELAY_NODE_TOKEN='<nt_live_… from fleet-enrollments.json>' \
RELAY_NODE_ID='node_210851746276208640' \
relay node up --broker-name kjg-laptop --background
Impact beyond one laptop
The same drift is visible across the fleet: nightcto-sf, nightcto-finn, nightcto-barry, and relayauth-finn-0803 all live in the auto-provisioned relay-6bf605d7 workspace rather than the workspace the Cloud dashboard reads, so their activity is invisible to fleet tooling. Diagnosing any of this currently requires reading the Rust broker log and resolving raw rk_live_* keys against /v1/workspace by hand.
Summary
A project workspace pin (
<repo>/.agentworkforce/relay/workspace-key.json) that lacks anenrolledNodeIdmakesrelay node upsilently ignore the fleet enrollment entirely.relay cloud enrollreports success, the node appears in the Cloud dashboard, and the node never heartbeats. Meanwhilerelay fleet nodesrun from that repo queries the pinned workspace, so it shows a completely different roster — with no error from either side.Net user experience: the dashboard shows node A,
fleet nodesshows node B,workspace activenames a third answer, and nothing anywhere says the three are different workspaces.Repro
Roster after the above —
kjg-laptopexists and has never heartbeat:Broker log (
~/Library/Logs/agentworkforce/relay/cloud.log.2026-08-05), same run:workspace_id=204337648549896192is the pinned auto-provisioned workspace — notrw_7ccfea89where the enrollment lives.Root cause
packages/cli/src/cli/commands/node.ts:112-131:fleet-enrollments.jsonis never consulted, noRELAY_NODE_TOKEN/RELAY_NODE_IDis set, and no warning is printed — the sibling branch atnode.ts:147-149does warn, this one doesn't. Locked in bynode.test.ts:239-257.There is also no workspace-id-vs-key reconciliation anywhere: the enrollment store holds workspace ids, the pin holds a key, so per the comment at
node.ts:166-169a mismatch is bypassed rather than detected. The broker then registers an auto-derived node in the pinned workspace (crates/broker/src/runtime/init.rs:305-330,:743-778), which is why it looks healthy.Proposed fixes
node.ts:125shouldwarn()before returning — e.g."project pinned to workspace <id>; ignoring N fleet enrollment(s). Run …". One line removes the entire class of confusion. Highest value / lowest risk.relay cloud enrollshould update the pin'senrolledNodeId(or refuse loudly when the repo is pinned to a different workspace). Today it writes onlyfleet-enrollments.json(commands/cloud.ts:920-927), so "enrolled into A while pinned to B" is a silently reachable state.workspace switch|joinshould preserveenrolledNodeId— they callwriteProjectWorkspaceKey(projectDataDir, workspaceKey)with no options (lib/workspace-session.ts:44-46), manufacturing exactly the broken state in fix fix(truncation): improvements #1.resumePinnedProjectWorkspacebails becauseRELAY_NODE_TOKENis set (lib/broker-lifecycle.ts:1327-1330), the broker auto-provisions a fresh workspace, and thenbroker-lifecycle.ts:1583-1586persists that throwaway key over the pin. Observed three separate free-plan workspaces all namedrelay-6bf605d7created for one repo:204337648549896192(07-18),210854606762164224,210855543429939200. The node plane stays correct while the agent plane silently drifts to an empty workspace.fleet statusshould match the roster bynodeId, not node name. It reported"no node named \"kjg-laptop\" in the workspace"while holding the correctnodeIdin the same payload, and separately matched an unrelated stale node that happened to share the repo-basename name.--workspace-key+ enrollment is an unguarded footgun. It disables enrollment pickup (node.ts:170-175), so the intuitive "just point it at the right workspace" fix mints a duplicate node instead of serving the enrolled one. Warn when both an enrollment and an explicit key are present.--broker-nameor it loops. WithRELAY_WORKSPACE_KEY+RELAY_NODE_TOKEN+RELAY_NODE_IDbut no--broker-name, the broker takes the repo basename and tries to rename the enrolled node to it:RELAY_NODE_IDor don't attempt the rename.Working invocation (verified)
Impact beyond one laptop
The same drift is visible across the fleet:
nightcto-sf,nightcto-finn,nightcto-barry, andrelayauth-finn-0803all live in the auto-provisionedrelay-6bf605d7workspace rather than the workspace the Cloud dashboard reads, so their activity is invisible to fleet tooling. Diagnosing any of this currently requires reading the Rust broker log and resolving rawrk_live_*keys against/v1/workspaceby hand.Environment
agent-relay11.4.0 (relay-broker/11.4.0), macOS (Darwin 25.5.0), 2026-08-05.