Skip to content

[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

Description

@serge-ivo

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:

export async function reassignSessionNode(env: Env, instanceId: string, userId: string, sessionId: string, runnerNode: string | null): Promise<void> {
	await env.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:

this.claudeSessionId = readState(config.statePath, config.id) ?? (config.resumeFrom ? readState(config.statePath, config.resumeFrom) : null);

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 (:888if (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:

  1. 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.
  2. Structural: [design] The platform owns the conversation — an engine's own memory is an optimisation, not the source of truth #693 — if the conversation lives in coding_timeline rather 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2: correctnessReal defect, no live harm today — inert fields, miscounts, missing guardsbackendBackend / Worker / API workbugSomething isn't workingcoderThe Coder wedge agent (#68) — Engine, Pilot, Co-pilot, Loop, Overseerrunnerpackages/browser-runner + packages/cli — ships only via an npm publish

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions