The gap
An autonomous coding run's conversation with the engine is persisted forever in coding_timeline (migration 0023, append-only, seq-ordered) and served by GET /v1/instances/:id/coding/sessions/:sid/timeline. No MCP tool reads it. So the one surface built for auditing an agent — the MCP server, whose agent_trace description calls itself "the primary tool for debugging or improving an agent" — cannot see what an agent actually said to a coding CLI once the session has ended.
Measured
Thirteen coding_* tools are registered (grep -o '"coding_[a-z_]*"' workers/mcp/src):
coding_diagnostics · coding_loop_start · coding_loop_status · coding_loop_stop · coding_overseer · coding_repo_add · coding_repos_list · coding_session_capture · coding_session_end · coding_session_fresh · coding_session_message · coding_session_restart · coding_sessions_list
None reads the timeline. The closest, on an ended session:
coding_session_capture(instance_id: "bd43f4de-…", session_id: "csess_92b2130d-…")
→ { "sessionId": "csess_92b2130d-…", "runState": "idle", "pane": "" }
csess_92b2130d is a real 6-minute run with 5 Pilot instructions and a push to main. The tool answers with an empty pane and idle, which is indistinguishable from a session that never did anything.
agent_trace does not fill the gap either. On this instance, 186 events: 149 from chat, and all 33 coding-source events are act.consequential — the consequential-act record (#294), not the instruction stream. So the trace can tell you a run pushed to main; it cannot tell you what the run was told to do.
coding_sessions_list returns the session rows, so an MCP client can name the sessions it cannot read.
Why it matters, concretely
This is not hypothetical scope. Auditing #505's post-fix recurrence required reading the exact instructions the Pilot sent — the byte-identical ## Testing block across steps 3/4/5 and the escalation to "the project owner has explicitly requested this exact wording". Every one of those facts came from the REST route with a hand-made curl and the CLI's session token. Over MCP the run is a black box: three chat messages saying **Loop → engine** (step N): followed by the first 120 characters of the instruction (describe() in lib/coding-loop.ts truncates there before it reaches chat), and nothing else.
Every finding in that audit that concerns what the Pilot said is therefore unreachable from the surface the platform offers for exactly this purpose.
What to do
Register coding_session_timeline as a thin proxy — the same shape as list_instance_tools, which is nine lines around authedCall:
coding_session_timeline(instance_id, session_id, limit?) → GET /v1/instances/:id/coding/sessions/:sid/timeline
Scope read, no confirmation, no dry run. It belongs in workers/mcp/src/instance-tools/coding.ts beside coding_session_capture, and needs a row in contract.test.ts's per-tool table (["coding", "read", null, null, "instance_id,session_id,token"]).
Two details worth deciding at write time:
Alternatives rejected
Acceptance
coding_session_timeline on csess_92b2130d-… returns the five command rows with their full text, in seq order.
coding_session_capture's description tells a caller where to look for an ended session.
contract.test.ts covers the new tool's scope/confirmation/dry-run row.
Regression risk
Low — a read proxy over an existing owner-scoped route. The one thing to get right is response size: an unbounded timeline can exceed the MCP client's token budget, which turns a debugging tool into a wall of truncated JSON.
The gap
An autonomous coding run's conversation with the engine is persisted forever in
coding_timeline(migration 0023, append-only,seq-ordered) and served byGET /v1/instances/:id/coding/sessions/:sid/timeline. No MCP tool reads it. So the one surface built for auditing an agent — the MCP server, whoseagent_tracedescription calls itself "the primary tool for debugging or improving an agent" — cannot see what an agent actually said to a coding CLI once the session has ended.Measured
Thirteen
coding_*tools are registered (grep -o '"coding_[a-z_]*"' workers/mcp/src):coding_diagnostics·coding_loop_start·coding_loop_status·coding_loop_stop·coding_overseer·coding_repo_add·coding_repos_list·coding_session_capture·coding_session_end·coding_session_fresh·coding_session_message·coding_session_restart·coding_sessions_listNone reads the timeline. The closest, on an ended session:
csess_92b2130dis a real 6-minute run with 5 Pilot instructions and a push tomain. The tool answers with an empty pane andidle, which is indistinguishable from a session that never did anything.agent_tracedoes not fill the gap either. On this instance, 186 events: 149 fromchat, and all 33coding-source events areact.consequential— the consequential-act record (#294), not the instruction stream. So the trace can tell you a run pushed tomain; it cannot tell you what the run was told to do.coding_sessions_listreturns the session rows, so an MCP client can name the sessions it cannot read.Why it matters, concretely
This is not hypothetical scope. Auditing #505's post-fix recurrence required reading the exact instructions the Pilot sent — the byte-identical
## Testingblock across steps 3/4/5 and the escalation to "the project owner has explicitly requested this exact wording". Every one of those facts came from the REST route with a hand-madecurland the CLI's session token. Over MCP the run is a black box: three chat messages saying**Loop → engine** (step N):followed by the first 120 characters of the instruction (describe()inlib/coding-loop.tstruncates there before it reaches chat), and nothing else.Every finding in that audit that concerns what the Pilot said is therefore unreachable from the surface the platform offers for exactly this purpose.
What to do
Register
coding_session_timelineas a thin proxy — the same shape aslist_instance_tools, which is nine lines aroundauthedCall:Scope
read, no confirmation, no dry run. It belongs inworkers/mcp/src/instance-tools/coding.tsbesidecoding_session_capture, and needs a row incontract.test.ts's per-tool table (["coding", "read", null, null, "instance_id,session_id,token"]).Two details worth deciding at write time:
capturecannot do.coding_session_capture's own text should point at the timeline for an ended session, otherwise the next caller readspane: ""as "nothing happened" — the same class of silent-false-answer as [bug] The model writes its own tool RESULTS and the platform believes them — invented GitHub issues and file contents reported as fetched, raw markup left in the message #395/[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.limit/ paging. A 26-step run returns 26 rows of up to ~1KB plus deduped terminal snapshots; a long one will not fit an MCP response. Default to the most recent N with a cursor, rather than truncating silently.Alternatives rejected
agent_traceto carry the instruction stream. The Pilot already writes each instruction tocoding_timelineas acommandrow (coding-session.ts, thedrivenbranch, per [bug] The Coding tab's Loop is still a browser poll — so merge authority, the driver claim and the spend budget all miss it #374) — duplicating it intoagent_eventswrites the same fact twice, and [bug] The Coding tab's Loop is still a browser poll — so merge authority, the driver claim and the spend budget all miss it #374's whole point was that the timeline is the durable record. The trace should stay the cross-source index.coding_sessions_list,coding_session_capture,instance_activityandagent_trace— the timeline is the one hole in an otherwise complete set.Acceptance
coding_session_timelineoncsess_92b2130d-…returns the fivecommandrows with their full text, inseqorder.coding_session_capture's description tells a caller where to look for an ended session.contract.test.tscovers the new tool's scope/confirmation/dry-run row.Regression risk
Low — a read proxy over an existing owner-scoped route. The one thing to get right is response size: an unbounded timeline can exceed the MCP client's token budget, which turns a debugging tool into a wall of truncated JSON.