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
instance_runtime_nodes.status is a write-once "online" column — every machine an instance has ever seen reports online forever, including one last seen four days ago #570
instance_runtime_nodes.status is a write-once "online" column — every machine an instance has ever seen reports online forever, including one last seen four days ago
Observed
instance_runtime_status(bd43f4de-ef35-4051-bdec-43f8571414a1), production, 2026-08-15. Four node rows, one machine actually connected:
runnerNode
runnerVersion
lastSeenAt
reported status
RLs-MacBook-Air
0.4.50
2026-08-14 23:13:08
online
Sergeys-Mac-mini.local
0.4.45
2026-08-12 07:43:42
online
RLs-MacBook-Air.local
0.4.47
2026-08-11 21:40:20
online
Mac
0.4.45
2026-08-10 20:27:28
online
Three of the four have not been seen in 2–4 days. All four say online. Reading the status field, an operator (or an agent) answering "which of my machines is up?" gets four yeses and one right answer.
Mechanism — the per-node table has no writer for any status but "online"
exportasyncfunctionupdateRuntimeStatus(env,instanceId,userId,status,runnerNode?){constnode=normalizeRunnerNode(runnerNode);if(node){awaitenv.DB.prepare(`UPDATE instance_runtime_nodes SET status = ?1, last_seen_at = … WHERE instance_id = ?2 AND user_id = ?3 AND runner_node = ?4`).bind(status,instanceId,userId,node).run();}awaitenv.DB.prepare(`UPDATE instance_runtimes SET status = ?1, …`)…}
The per-node UPDATE runs only when a runnerNode argument is supplied. Grep-complete list of the eight call sites:
routes/instances.ts:549 "online", normalizeRunnerNode(body.runnerNode) ← the only one with a node
routes/instances.ts:844 effective (no node)
routes/instances.ts:893 "offline" (no node)
routes/instances-tasks.ts:110 "online" (no node)
routes/instances-tasks.ts:112 "offline" (no node)
routes/instances-tasks.ts:116 "offline" (no node)
routes/instances-tasks.ts:605 "offline" (no node)
routes/instances-tasks.ts:608 "offline" (no node)
Exactly one call site passes a node, and it passes "online". Every offline write reaches instance_runtimes and never instance_runtime_nodes. So a node row's status is set once, at registration, and never changes — and GET /v1/instances/:id/runtime (routes/instances.ts:433) serialises that raw column straight out through runtimeNodeResponse, with no liveness check:
Why the routing code is fine and this still matters
Routing does not trust this column, deliberately. CLAUDE.md states it: resolution goes through getBoundRunnerConn → getLiveRunnerConn, "live-checked, not the DB status column which isn't cleared on disconnect". And the probe path of the same route (routes/instances.ts:840-887, ?probe=1) does the pin-aware live check that #380 built.
So work is routed correctly. What is wrong is the reporting: the un-probed nodes[] array is the answer instance_runtime_status gives over MCP, and it publishes a column the rest of the codebase knows is meaningless. The console does not read it — it uses /v1/instances/:id/runner-node (nodesDetail) and /v1/terminals/nodes, per store/console/src/lib/runnerPanel.ts:5,41 — which is why this has stayed invisible: the one consumer of the stale field is the debugging surface.
What to do — cheapest first
1. Do not serialise the column. Compute each node's status from last_seen_at against the same recency window routes/instances.ts:826 already uses for the single-runtime answer ("A runner heartbeats every 30s … if it was seen…"), or omit status from nodes[] entirely and let lastSeenAt speak. Either is honest; publishing a write-once field as a live one is not.
2. Or make the column true: pass the node through on the offline paths. More invasive — the offline writers at instances-tasks.ts do not know which node failed, and guessing would mark the wrong machine down. Prefer (1).
3. Say which node is live.instance_runtime_status already returns runtime.runnerNode for the default row; the useful field is a per-node live: boolean from relayConnected, which the probe path computes and the list path does not.
Alternatives considered and rejected
Prune stale node rows. They are the "Runs on" tile list and a pin target — config.runnerNode can legitimately name a machine that is currently off. Deleting history to fix a status field would break the pin UI.
Fix it in the MCP tool. The field is wrong at the route; the MCP tool is a pass-through, and the next consumer would inherit it.
Acceptance criteria
No API response reports a node online whose lastSeenAt is older than the heartbeat window.
On the instance above, exactly one node reads live.
Test: two node rows, one heartbeated now and one 3 days ago; assert the response distinguishes them.
Regression risk
config.runnerNode pins survive a machine being offline — a status change must not make a pinned-but-off machine disappear from the "Runs on" tiles or from /v1/terminals/nodes. Test that a pinned offline node is still listed, just not as online.
Recorded non-reproduction
The observation that started this was "my_instances pins config.runnerNode: "RLs-MacBook-Air" while the live node is "RLs-MacBook-Air.local", and list_errors shows 409s reading Runner node is not connected: RLs-MacBook-Air". That is not what is happening, and the pin is not broken.
normalizeRunnerNode (lib/runtime-nodes.ts:1-3) is trim().slice(0,120) — no .local stripping — so the two ARE different nodes with different relay DOs (bd43f4de…:node:RLs-MacBook-Air vs …:node:RLs-MacBook-Air.local, both present in the response above).
But the currently-connected node for this instance IS RLs-MacBook-Air, exactly matching the pin. The .local row is a stale 08-11 registration — which is only confusing because it also says online, which is this issue.
One physical laptop registering under two hostnames is real and is arguably worth its own decision, but it is a pags up hostname-resolution question, not a matching bug in the API.
Related: #537, #440 (a transport failure stored as durable state), #380 (the pin-aware liveness answer on the probe path).
instance_runtime_nodes.statusis a write-once "online" column — every machine an instance has ever seen reports online forever, including one last seen four days agoObserved
instance_runtime_status(bd43f4de-ef35-4051-bdec-43f8571414a1), production, 2026-08-15. Four node rows, one machine actually connected:RLs-MacBook-AironlineSergeys-Mac-mini.localonlineRLs-MacBook-Air.localonlineMaconlineThree of the four have not been seen in 2–4 days. All four say
online. Reading thestatusfield, an operator (or an agent) answering "which of my machines is up?" gets four yeses and one right answer.Mechanism — the per-node table has no writer for any status but "online"
Verified.
workers/api/src/routes/instances-runtime.ts:868-891:The per-node UPDATE runs only when a
runnerNodeargument is supplied. Grep-complete list of the eight call sites:Exactly one call site passes a node, and it passes
"online". Everyofflinewrite reachesinstance_runtimesand neverinstance_runtime_nodes. So a node row'sstatusis set once, at registration, and never changes — andGET /v1/instances/:id/runtime(routes/instances.ts:433) serialises that raw column straight out throughruntimeNodeResponse, with no liveness check:Why the routing code is fine and this still matters
Routing does not trust this column, deliberately.
CLAUDE.mdstates it: resolution goes throughgetBoundRunnerConn→getLiveRunnerConn, "live-checked, not the DBstatuscolumn which isn't cleared on disconnect". And the probe path of the same route (routes/instances.ts:840-887,?probe=1) does the pin-aware live check that #380 built.So work is routed correctly. What is wrong is the reporting: the un-probed
nodes[]array is the answerinstance_runtime_statusgives over MCP, and it publishes a column the rest of the codebase knows is meaningless. The console does not read it — it uses/v1/instances/:id/runner-node(nodesDetail) and/v1/terminals/nodes, perstore/console/src/lib/runnerPanel.ts:5,41— which is why this has stayed invisible: the one consumer of the stale field is the debugging surface.What to do — cheapest first
1. Do not serialise the column. Compute each node's status from
last_seen_atagainst the same recency windowroutes/instances.ts:826already uses for the single-runtime answer ("A runner heartbeats every 30s … if it was seen…"), or omitstatusfromnodes[]entirely and letlastSeenAtspeak. Either is honest; publishing a write-once field as a live one is not.2. Or make the column true: pass the node through on the offline paths. More invasive — the offline writers at
instances-tasks.tsdo not know which node failed, and guessing would mark the wrong machine down. Prefer (1).3. Say which node is live.
instance_runtime_statusalready returnsruntime.runnerNodefor the default row; the useful field is a per-nodelive: booleanfromrelayConnected, which the probe path computes and the list path does not.Alternatives considered and rejected
config.runnerNodecan legitimately name a machine that is currently off. Deleting history to fix a status field would break the pin UI.Acceptance criteria
onlinewhoselastSeenAtis older than the heartbeat window.Regression risk
config.runnerNodepins survive a machine being offline — a status change must not make a pinned-but-off machine disappear from the "Runs on" tiles or from/v1/terminals/nodes. Test that a pinned offline node is still listed, just not as online.Recorded non-reproduction
The observation that started this was "
my_instancespinsconfig.runnerNode: "RLs-MacBook-Air"while the live node is"RLs-MacBook-Air.local", andlist_errorsshows 409s readingRunner node is not connected: RLs-MacBook-Air". That is not what is happening, and the pin is not broken.normalizeRunnerNode(lib/runtime-nodes.ts:1-3) istrim().slice(0,120)— no.localstripping — so the two ARE different nodes with different relay DOs (bd43f4de…:node:RLs-MacBook-Airvs…:node:RLs-MacBook-Air.local, both present in the response above).RLs-MacBook-Air, exactly matching the pin. The.localrow is a stale 08-11 registration — which is only confusing because it also saysonline, which is this issue.list_errors(2026-08-14 21:18, andRunner node is not connected: Macon 08-13) are on different instances (f8ddc272,e4d2d031), whose own node rows I did not enumerate. They are consistent with [bug] A session stamped to an offline machine makes the Coding banner tell an owner already runningpags upto runpags up(#531 AC 2) #537 (a session stamped to an offline machine) rather than with a normalisation bug.One physical laptop registering under two hostnames is real and is arguably worth its own decision, but it is a
pags uphostname-resolution question, not a matching bug in the API.Related: #537, #440 (a transport failure stored as durable state), #380 (the pin-aware liveness answer on the probe path).