Repository navigation
Description
The engine plane cannot be read truthfully: capture strips the disambiguating evidence, advertises impossible values, and diagnostics calls a refusing engine healthy
Three defects on one question — is the engine actually working? — which is the half of #580 that was never closed.
1. MCP strips exactly the fields that make idle falsifiable
workers/mcp/src/index.ts:854 projects the response to {sessionId, runState, pane}, dropping runnerConnected, alive, ready, authPrompt.
The API returns runState: "idle" for three different worlds and disambiguates with precisely those siblings: routes/coding.ts:234 (no connection) and :240 (runner never answered) both collapse to idle. So over MCP, "the engine is idle", "the runner is gone" and "the probe failed" are one indistinguishable answer.
2. The tool advertises two values it cannot emit
index.ts:840 — "Also returns run state (idle/working/offline)."
The runner's vocabulary is "idle" | "thinking" | "responding" (packages/browser-runner/src/coding/runtime.ts:82). working is not a member, and offline is manufactured only by the timeline route (routes/coding-feed.ts:82), never by /capture.
coding-feed.ts:74-85 has the corrected vocabulary (ended/offline/unknown) with an explicit comment that unknown must not collapse into idle — so two surfaces sit over one probe with two vocabularies, and the older one is the one MCP exposes.
3. coding_diagnostics reports 0 issues for a stalled engine
Measured live on bd43f4de: healthySessions: 1, issueCount: 0, issues: [], live.alive: true, runState: "idle" — while the captured pane's last two lines are You've hit your weekly limit · resets Aug 17 at 4pm and [error].
The same instance's board says "Needs you" and its run says engine_limit. Both are right. The issue rule (lib/coding-diagnostics.ts:245-251) fires only on runner/relay faults, so an engine that is up but refuses to work is invisible to the tool whose stated purpose is "debug why sessions are offline or stuck".
Why this misleads into a wrong action
An owner debugging a stuck agent is sent to the tool named for it, told everything is healthy, and given a runState that cannot distinguish "thinking hard" from "the machine is gone". This is the exact question that cost 4.35 hours on 2026-08-15.
Acceptance criteria
coding_session_capturecarries enough to distinguish engine-idle, runner-absent and probe-failed. If payload size is the reason for the projection, say so and choose deliberately —index.ts:854currently drops them silently.- The description names only values the code can emit. A guard asserts the advertised vocabulary equals the emitted one, denominator stated per ADR 0002 over every tool that publishes a state enum — this is the third such mismatch found today.
- One vocabulary over one probe:
coding-feed.ts:74-85androutes/coding.ts:234,240agree, andunknownnever collapses intoidle. coding_diagnosticsreports an engine that is up but refusing as an issue, with the reason.issueCount: 0beside a board card reading "Needs you" is the defect.- Red demonstrated on the live case: a weekly-limit pane must not yield
issueCount: 0.
Verified vs inferred
- Verified: every
file:line; the livebd43f4devalues; the runner's three-member union; thecoding-feed.tscomment. - Inferred: that the projection at
index.ts:854was for payload size. Not stated in the code — AC1 should establish the intent before widening it.
Activity
fix(coding): the engine plane answers with one vocabulary, keeps the …
serge-ivo commentedon Aug 15, 2026
Shipped in
927f963(onmain, CI + API/MCP/Host deploys all green).AC1 — the projection's intent, established before widening it
The issue inferred the projection at
index.ts:854was for payload size and asked that the intentbe established first. It was not size.
git log -L 854,856:workers/mcp/src/index.tsputs the three-field shape in the tool's originalcommit —
d63333d, 2026-06-28, "read live terminal output + run state". So:alive/ready/runnerConnectedwere already on the API route fromf0785b1(2026-06-26),two days before the projection was written. They were available and simply not carried.
authPromptarrived on 2026-08-04 (260dec1,78383be) — five weeks after. It could nothave been dropped; nobody revisited the tool.
pane— up to 64 KB — whiledropping four booleans. Nothing in the commit or the code mentions size.
coding_session_capturenow returnsrunnerConnected,alive,readyandauthPromptbesiderunStateandpane.AC3 — one vocabulary over one probe
lib/coding-run-state.ts(new) iscoding-feed.ts's comment made executable and shared:idle|thinking|respondingis the engine's own union (derived and guarded againstruntime.ts:82), andended|offline|unknownis what the platform reports when no engine wasreached.
routes/coding.ts:234and:240now answerofflineandunknowninstead ofidle,and
coding-feed.tsresolves through the same function. An unrecognised word from a runner becomesunknownrather than passing through, so a value no engine can emit cannot reach a client byriding the snapshot.
Worth noting: this removes a console workaround.
agents/coder/web/src/repo-status.tsexistspartly because
/capturereported a vanished machine as an idle engine, so a repo row read "Ready",in green, under the tab's own "run
pags up" banner. Its reconciliation is unchanged and stillneeded (
runnerOnlineis on its own 10s timer), but it already handledstate === "offline", sothe honest value makes it more correct, not less. Its comments no longer document a contract that
has since changed.
AC2 — the guard, and its denominator
Two mechanisms, because a guard alone would not have prevented this:
runStateSentence()) — theshape
list_instance_toolsadopted for its tiers afterlist_instance_toolsdocuments 2 of the 4tiervalues it returns, and its default response is 117 KB — enough to exceed a host's limit on one instance #569. A sentence built from thevocabulary cannot name a value the code does not have.
state-vocabulary.test.tssweeps every registered tool (136, driven througha real registration) for enum-shaped claims and finds 12 value-set claims. Each must be
either backed by a vocabulary derived from the emitting source, or recorded in
UNBACKED_CLAIMSwith the reason. That inventory is a ratchet, not an allowlist: a stale entry fails too, so
it cannot rot.
Denominator stated per ADR 0002: 136 tools swept, 12 value-set claims, 1 backed vocabulary, 9
inventoried. G4 evidence: the scanner is proven on the literal defective string
(idle/working/offline), which it reduces toworkingas unemittable.Honest about scope: 9 of the 12 claims are emitted by code the MCP worker cannot import
(feedback status, trace source, write-consent, board view, …). They are inventoried, not verified,
and the file says so — pretending otherwise would be the "certifies ground it never walked" failure
ADR 0002 exists for. Backing them is a follow-up, and each entry names where its source lives.
AC4/AC5 — a refusing engine is an issue
refusingEngineIssuereports an engine that is up but not working, andhealthySessionsnow meansable to work rather than merely running — the two numbers a reader checks first were both wrong
in the same direction.
One deliberate deviation from AC5's wording. The verdict comes from the run, not the pane.
engine_limitis already a first-classwaitingReason(#541), so reading it is an observation;grepping a terminal buffer for "limit" would be a guess about free text written by whichever CLI
the user chose — and the diagnostics route only receives
paneLines(a count), not the pane text,so a pane rule was not even available without changing the runner protocol. The live
bd43f4decase had both signals, and it is pinned as a test: a run parked on
engine_limitcan no longeryield
issueCount: 0. A hand-driven session with no run still reports nothing, because inventinga verdict for it would be a guess.
No
MCP_SERVER_VERSIONbump: the surface fingerprint excludes descriptions and covers no responsebody, and this tool declares no
outputSchema.conformance.test.tsstays green on the 0.1.4 lock.Verified / not verified
Verified: full suite green (9014); 14 new API-side tests + 8 guard tests; typecheck, biome,
file-size (pin raised deliberately, +4 for the projection, with the reason recorded), bare-catch,
surface-lock, migrations, test-isolation, design-tokens, agents-allowlist, qa-config, docs-drift;
Playwright 405 passed (chromium + webkit); the four script gates re-run against the exact commit in
a throwaway worktree; CI and all three deploys green; both
/healthendpoints live (MCP stillreports 136 tools — no tool added).
NOT verified in production: the MCP endpoint is OAuth-gated and I hold no session token, so I
could not call
coding_session_capturelive to see the four new fields, norcoding_diagnosticson
bd43f4deto see it stop reportingissueCount: 0. Leaving this open for that check — itneeds an authenticated MCP call, which the
pags-liveagent can make.serge-ivo commentedon Aug 15, 2026
Verified live —
coding_diagnosticsnow reports the refusing engineRead from production 2026-08-15 on
bd43f4de(Chess coder 2), the instance this issue measured:issueCounthealthySessionsissues[]warnThe issue text:
Cause and the action, on the tool whose stated purpose is debugging stuck sessions. The AC5 deviation recorded in
927f963— deriving the verdict from the run'swaitingReasonrather than from the pane, because the diagnostics route receives onlypaneLines(a count) — produces the right answer here and is visible in the wording ("the run is parked", an observation, not an inference about the terminal).The session's
liveblock readsalive: true, runState: "idle"alongside it, so the two facts now coexist without contradiction: the runner is up, the engine is idle, and the run is parked. That is the distinction #580 was filed over, readable from one call.Still unverified on this issue
coding_session_captureansweringoffline/unknowninstead ofidleis not exercised here — this runner is connected, so the path that used to collapse runner absent and probe failed intoidlewas not reached. It needs a disconnected runner to confirm, which is a deliberate act on the owner's machine rather than something to manufacture.Verified vs inferred
fix(mcp): the health vocabulary is rendered from the enum that emits …
fix(mcp): the vocabulary inventory's citations are resolved against t…
feat(guards): every console capability is reachable over MCP, deliber…
serge-ivo commentedon Aug 18, 2026
Audit verdict — DELIVERED, all five criteria, re-verified against
mainand production todayRe-read the code, not the comments. Per criterion:
AC1 — the siblings that make
idlefalsifiable are carried.workers/mcp/src/coding-tools.ts(the tool moved out ofindex.ts) now declares and returnsrunnerConnected,alive,ready,authPromptbesiderunStateandpane, with the projection's history recorded in the comment above the handler. The "was it payload size?" question AC1 asked to settle first was answered in927f963f's comment withgit log -L: it was not, and the decisive evidence is that the projection kept a 64 KBpanewhile dropping four booleans.AC2 — the description names only emittable values.
runStateSentence()(workers/mcp/src/state-vocabulary.ts:135-137) RENDERS the sentence fromCODING_RUN_STATES, so the string cannot name a value the constant does not hold.workers/mcp/src/state-vocabulary.test.tssweeps every registered tool for enum-shaped claims and requires each to be backed or recorded inUNBACKED_CLAIMS— a ratchet, so a stale entry fails too.AC3 — one vocabulary over one probe.
workers/api/src/lib/coding-run-state.ts:49is the single union (ENGINE_RUN_STATES+PROBE_RUN_STATES), and all three readers resolve throughresolveRunState:routes/coding.ts:242→offline(no connection),:259→unknown(probe failed),:332-338normalises the runner's own word after the spread, so an unrecognised value becomesunknownrather than riding through;routes/coding-feed.ts:146,148,150→ the same function.Measured live today,
GET /v1/instances/{bd43f4de,3cbb1d1d}/coding/timeline?limit=1→"runState": "ended". That is aPROBE_RUN_STATESmember reaching a client; on the code this issue was filed against, both would have readidle.AC4 — a refusing engine is an issue.
refusingEngineIssue(lib/coding-run-state.ts:135-153) is called fromroutes/coding-diagnostics.ts:274-281for every active session, and derives the verdict from the run'swaitingReasonrather than from pane text.AC5 — red demonstrated on the live case. Recorded on this issue 2026-08-15:
bd43f4dewentissueCount 0 → 1,healthySessions 1 → 0, with the reason and the fix in the message.Two things to carry forward rather than lose in the close
refusingEngineIssuelacks therun.status !== "running"gate its siblinglib/work-report.ts:288has, so a CLOSED run that endedneeds_humanstill reads as a park. That is the inverse over-shoot, and it is tracked separately — it is not a reason to reopen this.offline/unknowncapture paths remain unexercised in production. The 2026-08-15 comment said so and it is still true: measured today, every coding session across all 20 coding instances is ended and the runner is connected, so neither branch is reached. Unit-covered, not production-observed. Recording that rather than implying otherwise.Closing.