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
[bug] A coding session relocated to another machine silently starts with an empty conversation — the resume pointer is a file on the machine it left #694
A coding session can legitimately move to a different machine while staying active. Its resume pointer cannot move with it, because the pointer is a file on the machine it left.
The two halves that disagree
The cloud half — relocation is one D1 column.lib/coding-store.ts:550:
exportasyncfunctionreassignSessionNode(env: Env,instanceId: string,userId: string,sessionId: string,runnerNode: string|null): Promise<void>{awaitenv.DB.prepare("UPDATE coding_sessions SET runner_node = ?4, updated_at = datetime('now') WHERE id = ?1 AND instance_id = ?2 AND user_id = ?3",)
The session keeps its id and its status. lib/coding-session-lifecycle.ts:59 is explicit that this is intended: "reassignSessionNode leaves the status alone, so a session legitimately relocated to a live machine can sit suspended. Treating it as a stop would end a healthy run at step 0."
The runner half — the pointer is local.coding/headless.ts:266:
readState (:938) reads loadFile(path)[id] from statePath, set to defaultStatePath(this.reposBaseDir) (coding/runtime.ts:238) — a JSON file on that runner's own disk. It maps our session id to Claude Code's session id, and the conversation behind it lives in ~/.claude on that same machine.
The failure
Session relocates from machine A to machine B. On B, readState(statePath, config.id) finds no entry — B's state file never had one. resumeFrom does not help: it is looked up in the same local file. buildClaudeArgs therefore appends no --resume (:888 — if (resumeId) args.push("--resume", resumeId)), and the engine starts with an empty conversation.
Nothing reports this. The session row still says active, the id is unchanged, and the console shows a live session. The user is mid-piece-of-work and the agent has quietly forgotten everything.
It is the exact failure #408 was written to prevent — its module header calls out that the pre-#408 behaviour was "reap, then re-open → always fresh, silently" — reached by a different route that #408 did not cover. resolveSessionContinuity never runs on this path at all: relocation is not an open.
Verified vs inferred — read this before estimating
Verified by reading the code: the SQL above touches one column; statePath resolves to a local path with no sync anywhere (grep -n statePath packages/browser-runner/src/coding/*.ts returns 5 hits, all local reads/writes in headless.ts and runtime.ts); --resume is appended only when the local lookup returns a value.
NOT reproduced. I have not run a two-machine relocation and watched the conversation drop. That test is available and cheap — the account has two known machines (Sergeys-Mac-mini.local, RLs-MacBook-Air.local) and instance bd43f4de is pinned to the mini. Someone should run it before this is closed, because the whole finding rests on "nothing copies the file", which is an absence.
Fix
Two options, and the second is the real one:
Narrow: when relocating, either carry the resume mapping or explicitly mark the session as having lost continuity, so the loss is at minimum reported rather than silent. This is a patch on a design that will keep producing this class.
A coding session can legitimately move to a different machine while staying active. Its resume pointer cannot move with it, because the pointer is a file on the machine it left.
The two halves that disagree
The cloud half — relocation is one D1 column.
lib/coding-store.ts:550:The session keeps its
idand itsstatus.lib/coding-session-lifecycle.ts:59is explicit that this is intended: "reassignSessionNodeleaves the status alone, so a session legitimately relocated to a live machine can sit suspended. Treating it as a stop would end a healthy run at step 0."The runner half — the pointer is local.
coding/headless.ts:266:readState(:938) readsloadFile(path)[id]fromstatePath, set todefaultStatePath(this.reposBaseDir)(coding/runtime.ts:238) — a JSON file on that runner's own disk. It maps our session id to Claude Code's session id, and the conversation behind it lives in~/.claudeon that same machine.The failure
Session relocates from machine A to machine B. On B,
readState(statePath, config.id)finds no entry — B's state file never had one.resumeFromdoes not help: it is looked up in the same local file.buildClaudeArgstherefore appends no--resume(:888—if (resumeId) args.push("--resume", resumeId)), and the engine starts with an empty conversation.Nothing reports this. The session row still says
active, the id is unchanged, and the console shows a live session. The user is mid-piece-of-work and the agent has quietly forgotten everything.It is the exact failure #408 was written to prevent — its module header calls out that the pre-#408 behaviour was "reap, then re-open → always fresh, silently" — reached by a different route that #408 did not cover.
resolveSessionContinuitynever runs on this path at all: relocation is not an open.Verified vs inferred — read this before estimating
Verified by reading the code: the SQL above touches one column;
statePathresolves to a local path with no sync anywhere (grep -n statePath packages/browser-runner/src/coding/*.tsreturns 5 hits, all local reads/writes inheadless.tsandruntime.ts);--resumeis appended only when the local lookup returns a value.NOT reproduced. I have not run a two-machine relocation and watched the conversation drop. That test is available and cheap — the account has two known machines (
Sergeys-Mac-mini.local,RLs-MacBook-Air.local) and instancebd43f4deis pinned to the mini. Someone should run it before this is closed, because the whole finding rests on "nothing copies the file", which is an absence.Fix
Two options, and the second is the real one:
coding_timelinerather than in a file on one laptop, relocation is not a special case at all. A session moves machines and the brief travels with it, because the brief is in D1.Prefer 2. File 1 only if 2 is not being taken soon, since a silent wrong answer is worse than a slow one.