Skip to content

[bug] Supervision can be wired on an agent that cannot delegate — the picker offers it and the route answers 201 #354

Closed

Description

@serge-ivo

Two halves of supervision that never check each other

Supervision is split across two scopes, correctly:

  • The graph is instance-level — agent_supervision (migration 0060) maps
    supervisor_instance_id → subordinate_instance_id. You wire two of your instances together.
  • The ability is agent-level — delegate_goal, list_subordinates, subordinate_status,
    check_delegation are supervision registry tools, and an agent only receives them if its
    capabilities.tools declares them (toolNamesFor treats a declared list as authoritative).

Exactly one agent declares them: coder-lead (migrations 0063, 0067).

Nothing joins the halves:

The UI offers the wiring to everyone. TeamworkSection.tsx renders "Agents this one
supervises"
with a live picker on every instance's Settings tab. It is mounted unconditionally from
SettingsTab.tsx:1080. On this account that is 26 instances, 25 of which cannot delegate.

The route agrees with the UI, not with the runtime. POST /v1/instances/:id/supervision
(routes/tools.ts:679-696) checks ownership and then the graph rules — no self-edge, no cycle, one
supervisor per subordinate, depth ≤ 4, fan-out ≤ 12. Every one of those is a real check and they are
right to be there. But it never asks whether the supervisor's agent can delegate at all. It returns
201 and writes the row.

What that produces

A user picks a subordinate on, say, Repo Chat. The picker accepts it, the row appears under "Agents
this one supervises", the API said 201. Then they tell the agent to delegate, and it cannot — the tool
was never in its allowlist, so it does not exist from the model's point of view. The agent's best
available behaviour is to explain that it can't, which reads as the agent being broken rather than the
configuration being impossible.

This is the same shape as #351: a surface reporting a capability the runtime will refuse. There
the tool listing said allowed=true, reason=ok for a write-gated tool; here the supervision editor
and its route both say yes to an edge that can never be used.

Fix

Route — in POST /:id/supervision, resolve the supervisor instance's capabilities and 400 when
its tool set contains no supervision connector tool. The message should name the reason
("this agent cannot delegate — its capabilities declare no supervision tools"), consistent with the
other wiring-time rejections, which are deliberately loud because the human is present. That
sentence is already in migration 0060's header and it applies here.

Console — gate the Supervision half of TeamworkSection on the same fact, from the resolved
capabilities that /v1/instances/my/instances already attaches to each instance. Connections and
Deliveries stay ungated: any agent can emit a fact, and any agent can be a target.

Note the two halves must agree in both directions — a picker that offers nothing is fine, a
picker that offers something the route rejects is not, and neither is a route that accepts what the
picker would have hidden.

Out of scope, but worth naming

Whether the subordinate needs anything declared. It does not today, and probably shouldn't — being
delegated to is not a capability, it is just being an instance. Recording that here so the next person
doesn't add a symmetric check by reflex.

Related: #351 (same class), #183 (the graph), #185 (subordinate executes with its own tools/consent).

Activity

  1. added
    bugSomething isn't working
    coordinationAgent coordination / delegation primitives
    on Aug 7, 2026
  2. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Sequence for the settings-scope set filed together (#352, #353, #354, #355): #353 → this → #355 → #352.

    This one is independent of the other three — it touches the supervision route and TeamworkSection, not the connector blocks — so it can land in any order. It is placed second only because it is small and verified.

    Verification recorded, since the load-bearing claim was worth checking twice: the risk was that an agent declaring no capabilities.tools might receive supervision tools by default, which would make "25 of 26 cannot delegate" false. It does not. toolNamesFor (agent-do-tools.ts:183) falls back to FULL (:111) = BASE + KB_READ + KB_WRITE + FILES + COLLECTIONS + APPLY + CODING — no connector tools, so no supervision tools. Checked against live data too: delegate_goal appears in exactly one of 26 subscribed instances (coder-lead).

  3. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Scoping note, verified while filing #358: the route fix covers MCP for free. create_supervision in workers/mcp/src/instance-tools/composition.ts:72 is a thin proxy — it calls authedCall("/v1/instances/:id/supervision", …, {method:"POST"}) rather than reaching lib/supervision.ts or D1 directly.

    So the two-part fix in the body is complete as written: the validation in POST /:id/supervision closes the API and MCP together, and the TeamworkSection gate is purely so the picker stops offering what the route now rejects.

    Its dry_run text will also want updating — it currently enumerates the rejections ("supervision loop, depth or fan-out limit, second supervisor") and would need "or the supervisor declares no supervision tools" to stay accurate.

  4. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Landed in 3229460.

    Claims checked, all held. POST /v1/instances/:id/supervision checked ownership and then only the graph rules — no capability check anywhere in the path — and TeamworkSection was mounted unconditionally from SettingsTab, picker and all.

    Route. delegationDenial(await capabilitiesForInstance(...)) → 400 "This agent cannot delegate — its capabilities declare no supervision tools…", placed with the cycle / tower / fan-out / two-managers rejections and for the reason they state: invisible at run time until it has already cost money, and the human is present now. Same argument create_connection makes for validating its filter at create time.

    Both halves read the same fact from the same source, so they cannot drift into disagreeing — which is the failure mode you named. The route asks the registry (getConnector("supervision").tools); the console asks GET /v1/instances/:id/tools, which carries each tool's registry connector. A fifth supervision tool or a rename needs no edit in either. No copied list of tool names exists in this change.

    Console. Supervision is offered only to an agent that can delegate; Connections and Deliveries stay ungated, per the ticket. Two judgement calls worth naming:

    • Existing links are still listed on an agent that cannot delegate, with a note saying they do nothing. There are rows already written by the old route, and a row you cannot see is a row you cannot remove. The picker is what disappears.
    • Gated on declared, not allowed: an owner who switched the delegation tools off has paused the wiring, not made it impossible.

    Failure-read direction. A capabilities lookup that comes back empty ALLOWS. Ownership is proven before that line runs, so a null is a failed read rather than evidence — refusing on it would turn a D1 blip into "your agent cannot delegate", which is this ticket's own bug pointed the other way. The runtime gate is untouched either way.

    Left alone as you asked: the SUBORDINATE needs nothing declared, and create_supervision's MCP description now says so explicitly so nobody adds the symmetric check by reflex.

    CI green.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcoordinationAgent coordination / delegation primitives

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions