A healthy repo has been marked broken for five days, and nothing has re-asked
pas/platform on the Coder Home instance says the checkout cannot be used. The checkout is fine.
The verdict was taken on 2026-08-03 by something that was not looking at the checkout at all, and
nothing has replaced it since.
Measured, production, twice, ten minutes apart
GET /v1/instances/12ebf1f0-…/coding/repos
pas/platform cloneStatus: "error"
cloneError: "No runner connected — run `pags up`"
workdir: ~/dev/stores/pas/platform
updatedAt: 2026-08-03 01:44:25 ← five days ago
On disk, right now: ~/dev/stores/pas/platform holds 18 entries and
git rev-parse --is-inside-work-tree → true. The machine is connected —
GET /v1/relay/12ebf1f0-…/status?node=Mac → {"connected":true}, runner 0.4.44 (which has
/coding/repo-check). Two consecutive reads of the repos list left the row byte-identical.
updateRepoClone bumps updated_at on every write (lib/coding-store.ts:155-171), so the
untouched timestamp is proof that no verdict has been written for this row in five days, not
merely that none changed it.
Meanwhile the chat prompt reads that row and tells the model the repository is unusable
(lib/repo-status-prompt.ts, attachedReposPrompt). #405 exists because a wrong ready makes an
agent invent code; a wrong error makes it refuse work it could do.
Two defects, and one correction to the report
1. A transport failure is stored as a property of the repo.
lib/coding-session-open.ts:112-120:
} catch (e) {
const msg = e instanceof Error ? e.message.slice(0, 300) : String(e);
await updateRepoClone(env, repo.id, { cloneStatus: "error", cloneError: msg });
Any exception from POST /coding/start becomes the repo's clone status — including
No runner connected, which is a fact about a WebSocket, not about a directory. clone_status
answers "is there usable code at this path"; a closed laptop is not an answer to that question.
The platform already models this distinction correctly one file over: verdictFromCheck has a
dedicated unverified state for exactly "the machine could not tell us", and
cloneStatusForVerdict returns null for it — deliberately no write (lib/coding-workdir.ts:73-84, 122-133). runnerUnreachable-shaped failures should take that path, not the error path.
2. There is no way to re-take a verdict, and no record of when one was taken.
A correction first, because it changes what to build: the repo list DOES re-verify —
routes/coding-repos.ts:139-155 runs verifyLocalWorkdir over up to MAX_VERIFY_PER_LIST = 12
local repos on every GET …/coding/repos. So "the check only runs at add and at workdir change" is
not true of the code. What is true is that the re-check is:
- conditional —
if (!conn) return c.json({ repos }). No resolvable runner connection at that
instant, no re-check, no trace of the attempt;
- incidental — the only trigger is someone opening that instance's repo list. A row belonging to
an instance nobody opens is never re-examined by anything;
- one-directional in practice —
onUnverified: null at list time is right (an offline laptop
must not condemn a path it verified yesterday), but it means the only way out of a wrong verdict is
a successful check, which requires the conditional path above to fire.
And an open question I could not close. With the relay reporting connected: true for that very
instance and a 0.4.44 runner, two list reads still wrote nothing — so either
getBoundRunnerConn returned null (a pin, a registration row under another node name, #379
alias resolution) or /coding/repo-check answered unverified. Both are invisible: the route
neither logs the attempt nor reports it in the response. Whoever picks this up should instrument
that first — it may be the whole bug, and it is unknowable from outside. For contrast, another
instance's row (bd43f4de… / apps/chess-academy) was written today at 06:21:55, so the write
path does work somewhere.
A second live row, reported as the same thing but genuinely different
stores/fds (same instance, workdir ~/dev/stores/fds) reads ready; the directory holds one
entry and no .git of its own. That is not a stale verdict — it is what today's check would
say: ~/dev/stores is itself a git work tree, so git rev-parse --is-inside-work-tree is true
for the subfolder, and #405 chose that test on purpose ("~/dev/monorepo/apps/thing is a
legitimate workdir and an existence test would have condemned every package in every monorepo
checkout"). Worth knowing while working here, not worth changing on this evidence.
What to do — cheapest first
- Never write
clone_status: "error" for a transport failure. In coding-session-open.ts:117-120,
route an unreachable-runner exception (isRunnerUnreachable / relayFailureIsDisconnect already
exist in lib/runner-availability.ts) to no write — the same answer unverified gets. A repo's
state should only be written by something that looked at the repo.
- Add
clone_checked_at (migration + a write in verifyLocalWorkdir). repo-status-prompt.ts:113-129
already asks for it and explains why updated_at cannot stand in:
"updated_at is bumped by any edit to the row (rename, launch URLs, merge policy), so rendering
it as 'checked 3 minutes ago' would be a precise-looking claim the platform cannot actually
support … A real clone_checked_at column would fix that."
With it, FRESHNESS_NOTE can say when instead of only "the LAST RECORDED verdict", the
repo row can show it, and "this verdict is five days old" becomes visible rather than
reconstructed from a timestamp that means something else.
- Make re-checking reachable on purpose. A
POST …/coding/repos/:id/recheck (or a "Re-check"
control beside the diagnosis banner ReposList.tsx already renders) that runs verifyLocalWorkdir
with onUnverified: "unknown" and returns the verdict — so a user looking at a wrong verdict has
an action, and so the check can be exercised deliberately when diagnosing.
- Say when a re-check did not happen. When the list route returns early for want of a
connection, that is information: the rows shown are last-known, not current. It is one field on
the response and one sentence in the UI, and it is what makes the difference between "the
platform checked and it is broken" and "nobody has looked since Monday".
Alternatives considered and rejected
Acceptance criteria
Regression risk
clone_status is a plain string column with an additive union (ready / error /
needs_attention / unknown); readers are repo-status-prompt.ts, repo-status.ts and
ReposList.tsx. Adding a column is safe; changing what a value means is not — leave the
existing four alone.
- Not writing on a transport failure means a genuinely broken clone that also happens to fail at the
transport keeps its previous status. That is the correct trade (silence over a false claim) and it
is the same trade onUnverified: null already makes, but it should be stated in the code, not
inferred.
- A
recheck route runs a relay command on user input: rate-limit it with the existing buckets and
scope it to the owner, like every other :instanceId route.
Files: workers/api/src/lib/coding-session-open.ts:112-120, workers/api/src/routes/coding-repos.ts:100-160,181,599,
workers/api/src/lib/coding-workdir.ts:73-133, workers/api/src/lib/coding-store.ts:155-171,
workers/api/src/lib/repo-status-prompt.ts:102-133, store/console/src/…/ReposList.tsx.
Related: #405 (which built the check; still open pending live confirmation — this is why reading
the list cannot confirm it), #416 (which decided against checking at chat time), #410/#411.
A healthy repo has been marked broken for five days, and nothing has re-asked
pas/platformon theCoder Homeinstance says the checkout cannot be used. The checkout is fine.The verdict was taken on 2026-08-03 by something that was not looking at the checkout at all, and
nothing has replaced it since.
Measured, production, twice, ten minutes apart
On disk, right now:
~/dev/stores/pas/platformholds 18 entries andgit rev-parse --is-inside-work-tree→true. The machine is connected —GET /v1/relay/12ebf1f0-…/status?node=Mac→{"connected":true}, runner 0.4.44 (which has/coding/repo-check). Two consecutive reads of the repos list left the row byte-identical.updateRepoClonebumpsupdated_aton every write (lib/coding-store.ts:155-171), so theuntouched timestamp is proof that no verdict has been written for this row in five days, not
merely that none changed it.
Meanwhile the chat prompt reads that row and tells the model the repository is unusable
(
lib/repo-status-prompt.ts,attachedReposPrompt). #405 exists because a wrongreadymakes anagent invent code; a wrong
errormakes it refuse work it could do.Two defects, and one correction to the report
1. A transport failure is stored as a property of the repo.
lib/coding-session-open.ts:112-120:Any exception from
POST /coding/startbecomes the repo's clone status — includingNo runner connected, which is a fact about a WebSocket, not about a directory.clone_statusanswers "is there usable code at this path"; a closed laptop is not an answer to that question.
The platform already models this distinction correctly one file over:
verdictFromCheckhas adedicated
unverifiedstate for exactly "the machine could not tell us", andcloneStatusForVerdictreturnsnullfor it — deliberately no write (lib/coding-workdir.ts:73-84, 122-133).runnerUnreachable-shaped failures should take that path, not theerrorpath.2. There is no way to re-take a verdict, and no record of when one was taken.
A correction first, because it changes what to build: the repo list DOES re-verify —
routes/coding-repos.ts:139-155runsverifyLocalWorkdirover up toMAX_VERIFY_PER_LIST = 12local repos on every
GET …/coding/repos. So "the check only runs at add and at workdir change" isnot true of the code. What is true is that the re-check is:
if (!conn) return c.json({ repos }). No resolvable runner connection at thatinstant, no re-check, no trace of the attempt;
an instance nobody opens is never re-examined by anything;
onUnverified: nullat list time is right (an offline laptopmust not condemn a path it verified yesterday), but it means the only way out of a wrong verdict is
a successful check, which requires the conditional path above to fire.
And an open question I could not close. With the relay reporting
connected: truefor that veryinstance and a 0.4.44 runner, two list reads still wrote nothing — so either
getBoundRunnerConnreturnednull(a pin, a registration row under another node name,#379alias resolution) or
/coding/repo-checkansweredunverified. Both are invisible: the routeneither logs the attempt nor reports it in the response. Whoever picks this up should instrument
that first — it may be the whole bug, and it is unknowable from outside. For contrast, another
instance's row (
bd43f4de…/apps/chess-academy) was written today at06:21:55, so the writepath does work somewhere.
A second live row, reported as the same thing but genuinely different
stores/fds(same instance, workdir~/dev/stores/fds) readsready; the directory holds oneentry and no
.gitof its own. That is not a stale verdict — it is what today's check wouldsay:
~/dev/storesis itself a git work tree, sogit rev-parse --is-inside-work-treeistruefor the subfolder, and #405 chose that test on purpose ("
~/dev/monorepo/apps/thingis alegitimate workdir and an existence test would have condemned every package in every monorepo
checkout"). Worth knowing while working here, not worth changing on this evidence.
What to do — cheapest first
clone_status: "error"for a transport failure. Incoding-session-open.ts:117-120,route an unreachable-runner exception (
isRunnerUnreachable/relayFailureIsDisconnectalreadyexist in
lib/runner-availability.ts) to no write — the same answerunverifiedgets. A repo'sstate should only be written by something that looked at the repo.
clone_checked_at(migration + a write inverifyLocalWorkdir).repo-status-prompt.ts:113-129already asks for it and explains why
updated_atcannot stand in:POST …/coding/repos/:id/recheck(or a "Re-check"control beside the diagnosis banner
ReposList.tsxalready renders) that runsverifyLocalWorkdirwith
onUnverified: "unknown"and returns the verdict — so a user looking at a wrong verdict hasan action, and so the check can be exercised deliberately when diagnosing.
connection, that is information: the rows shown are last-known, not current. It is one field on
the response and one sentence in the UI, and it is what makes the difference between "the
platform checked and it is broken" and "nobody has looked since Monday".
Alternatives considered and rejected
latency path of every message, for a fact that changes rarely.
per user per machine, so it would mostly write
unverifiedand spend relay traffic to learnnothing. Revisit only if (3) proves insufficient.
onUnverified: "unknown"). Rejected —coding-repos.ts:103-112argues it correctly: it would flip every repo in the list every time alaptop closed, and the offline banner already says the true thing.
moved. This is the rows that already existed, plus a writer [bug] A local repo is marked "ready" without anyone checking it exists — an empty checkout reports success, and the agent invents the code it cannot see #405 never touched.
Acceptance criteria
POST /coding/startthat fails because no runner is reachable leavesclone_statusunchanged (test: unreachable-runner error → no
updateRepoClonecall).coding_repos.clone_checked_atexists, is written by every verdict, and is surfaced in therepo row and in
attachedReposPrompt's freshness note.restart.
readyagain after a re-check, andstores/fdsstill readsready(the monorepo-subfolder case must not regress).
Regression risk
clone_statusis a plain string column with an additive union (ready/error/needs_attention/unknown); readers arerepo-status-prompt.ts,repo-status.tsandReposList.tsx. Adding a column is safe; changing what a value means is not — leave theexisting four alone.
transport keeps its previous status. That is the correct trade (silence over a false claim) and it
is the same trade
onUnverified: nullalready makes, but it should be stated in the code, notinferred.
recheckroute runs a relay command on user input: rate-limit it with the existing buckets andscope it to the owner, like every other
:instanceIdroute.Files:
workers/api/src/lib/coding-session-open.ts:112-120,workers/api/src/routes/coding-repos.ts:100-160,181,599,workers/api/src/lib/coding-workdir.ts:73-133,workers/api/src/lib/coding-store.ts:155-171,workers/api/src/lib/repo-status-prompt.ts:102-133,store/console/src/…/ReposList.tsx.Related: #405 (which built the check; still open pending live confirmation — this is why reading
the list cannot confirm it), #416 (which decided against checking at chat time), #410/#411.