Skip to content

[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

Description

@serge-ivo

The Coding tab's Loop button still runs in the browser (agents/coder/web/src/use-coding-loop.ts), self-scheduling with setTimeout and driving the Engine through POST …/coding/sessions/:id/message — the route documented as "manual drive, no brain."

It is not a manual drive. /loop-decide (BYOK Claude, cloud-side) writes nextInstruction and the browser relays it verbatim, up to 50 iterations, and in issues-mode it chains issue after issue. It is an autonomous driver wearing the human-typing path's clothes — so every guard the platform added for autonomous drivers is on the other side of it.

Three escapes, each verified at HEAD (9007758)

1. Merge authority (#314) does not apply. All three layers live only in the Pilot:

  • readMergePolicyForRunworkflows/coding-session.ts:84 (only call site)
  • screenInstructionlib/coding-loop.ts:150 (only enforcement call site; subordinate-config.ts:243 merely reports)
  • unauthorizedActs / recordAuthorityViolationsworkflows/coding-session.ts:182,403,695

routes/coding.ts:450 (/message) resolves no policy, screens nothing, and the mode:"watch" workflow it spawns (workflows/coding-watch.ts) checks no act against a policy — grep for mergePolicy|unauthorizedActs|screenInstruction there returns nothing. So on a repo set to pr or none, the model may emit "merge the PR" and the browser relays it to the Engine unrefused, and a merge that does happen halts nothing.

coding-session.ts:79-83 states the assumption this breaks in so many words: "the Pilot is the only thing that drives the Engine autonomously, so a single resolution point cannot drift and no caller can forget to pass it." At HEAD that is not true.

2. The one-driver-per-engine claim (#208, migration 0069) does not apply. claimSessionDriver guards /sessions/:id/run (coding.ts:548), drive_claude (coding-brains.ts:132) and the coding loop driver (loop-drivers.ts:159). /message has no claim — deliberately, because a human interjecting must always be able to. So the browser Loop can type into a pane a Pilot has claimed and is reasoning over: exactly the two-brains-one-pane failure the claim exists to prevent, reached from a button rather than from a race.

3. The delegation budget (#184) does not apply. /loop-decide (routes/instances-chat.ts:130) reserves nothing and settles nothing. An N-iteration browser Loop is N unbudgeted BYOK Claude calls plus N Engine turns.

Each iteration also creates a fresh CODING_SESSION watch workflow (coding.ts watchId stamp) — superseded, so probably harmless, but it is one workflow instance per loop step.

Why it reads as done

#158 migrated the Assistant tab off the browser poll (67eb915) and its closing comment says so precisely. Nothing claimed the Coding tab. The result is two buttons both labelled Loop, on the same agent, with opposite properties:

Assistant tab Loop Coding tab Loop
runs on AgentLoopWorkflow / Pilot the browser tab
survives tab close yes no
agent_loop_runs row yes — cancellable, observable none
driver claim yes (loop-drivers.ts:159) no
budget (#184) reserved + settled none
merge authority (#314) screened + halted neither

A supervisor reading check_delegation / subordinate_status sees nothing for a Coding-tab Loop, because there is no run row to see.

Not visibility — decision

#294 still holds on this path: /capture calls recordEngineActs on every poll regardless of who drives (coding.ts:229, with the comment saying that is on purpose). A merge is recorded. Nothing decides whether it was allowed, and nothing stops the second one.

Suggested shape

Point the Coding tab's Loop at POST /v1/instances/:id/loop — the same verb the Assistant tab uses — and let loop-drivers.ts dispatch to codingDriver, which already opens the run row, claims the driver, carries the budget and starts CodingSessionWorkflow with merge authority resolved. The hook becomes a watcher over /loop/:runId, mirroring InstanceDetail.tsx. Issues-mode's approve-per-issue gate stays client-side; only the drive changes.

That is the last unconverged piece of #154's fourth verified problem bullet ("autonomy that isn't durable"), but it is filed here rather than left in the epic because the escapes above are a safety defect on their own, independent of whether the coordination epic stays open.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions