Skip to content

Decision-model supervision: monitors on subagents, stops that reach the swarm, enforced gates, and 10 more gaps - #477

Merged
Broccolito merged 14 commits into
mainfrom
feat/decision-supervision
Nov 7, 2026
Merged

Broccolito merged 14 commits into
mainfrom
feat/decision-supervision

Conversation

@Broccolito

@Broccolito Broccolito commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

After #473 (decision models) and #474 (five fixes) merged, a comparison with the prototype in #469 found 13 gaps in how decision models supervise a chat and its subagents. This PR fixes all 13. #469 stays open as a reference and is not touched.

Item Gap What it does now
A No agent tool to put monitors on subagents workspace_monitor lets the agent add, list and remove monitors on its subagents. Monitors can also be set at spawn, come from a plan's phase, or be inherited from a person's monitor that "also watches subagents". A child's monitor stop reaches the parent's result. The agent cannot change or remove a monitor the person set. A private chat writing a monitor into a public child goes through the first-crossing disclosure.
B A stop on an orchestrator waits for its children One function stops every subagent a chat started. A plain Stop, a monitor stop and workspace_close use it; every stopped child records why. Stop-and-Send leaves children running.
C Advisory gates The agent can no longer skip a gated phase, drop it from a re-declared plan, or loosen or replace its gate; holds and depends_on are enforced at start and spawn. The person passes, skips or retries a gate (biorouter session gates, the phase gate dialog, HTTP routes with proof).
D The latch doesn't stop again after you continue A stop monitor stops again on every turn while its answer holds. rearmBelow / rearmAbove give the old stop-once behaviour.
1 Changes don't apply mid-turn Monitors are re-read at every check point, so a monitor added or edited during a long (swarm) turn applies in that turn.
2 The agent isn't told why it stopped The stop also stores an agent-only <decision-monitor-stop> note naming the monitor and its reason.
3 Some paths skip silently with pause on Unreadable settings, an empty or unchanged window, the steer grace and a tiny budget either check or say so; onError: pause stops the turn.
4 The desktop can't set onError pause, fire below, or a threshold under 0.50 Monitors dialog: thresholds from 0.01, "Fires when: Below", "If a check fails: Pause the turn", editable re-arm level. CLI: --below, --rearm-above, --on-error pause.
5 Private chats need a local decision model; Crew chats refuse every decision An install path for tev1:0.8b / clef-flash (biorouter models decision pull, an install panel in the Monitors dialog, /decisions/models/install routes), with honest sentences when none is installed or Ollama is not answering. Crew chats may use decision models that run on this computer; hosted ones are still refused.
6 Saves aren't checked A save whose monitor or gate cannot run for the chat (model, tier, key, not pulled) is refused with 422 and "Nothing was saved: ..."; malformed bodies stay 400; reach and proof stay 403.
7 The latch lives in memory The latch is stored on the session (decision_monitor_state.v1) and survives a daemon restart; editing the question resets it.
8 No reusable setup Workflow YAML declares decision_settings (monitors, a default decision model) and an orchestration_plan with gates. Every door that starts a workflow (run, /agent/start, schedules, slash commands, sub-workflows) seeds them once, refuses before creating a session when they cannot run, and --explain prints them; creating a workflow from a chat captures them.
9 A billed malformed answer isn't counted A billed answer that cannot be read keeps its tokens (decision row plus exactly one token_events row). A late answer after a timeout, or one whose caller went away, is settled once in the background and counted; the CLI and daemon wait for in-flight answers at exit.

Defaults that need the owner's eye

  1. A plain Stop on a chat also stops the subagents it started; Stop-and-Send (correcting the chat mid-run) leaves them running.
  2. Crew chats may use decision models that run on this computer; hosted ones are still refused. This widens what reads a Crew chat beyond its granted model (DR-26: nothing leaves the device), so it needs human review.
  3. Existing stop monitors change behaviour with no migration: they stop again on every turn while their answer holds. A re-arm level gives the old stop-once behaviour.
  4. After a timeout or a Stop, a billed decision request runs to completion (at most about 120 s) so its tokens are counted; its row can appear after the turn ended.
  5. The agent can no longer skip a gated phase or loosen its gate, so a run can wait for the person to pass, skip or retry the gate.

Status codes: 422 means "valid but cannot run for this chat now"; 403 stays for reach, proof and subagent refusals only, because the CLI reads an empty 403 as a missing user-action key.

Needs human review (CLAUDE.md: security-sensitive and async code)

  • The Crew admission change (decision/service.rs, crew/mod.rs).
  • Late and abandoned answers (decision/late.rs, decision/service.rs): background settlement, exit waits.
  • Reply-loop waits and the swarm stop (agents/agent.rs, subagent_handle.rs, routes/reply.rs cancel).
  • The first-crossing disclosure for workspace_monitor (workspace_inspector.rs).

Live verification

Every tester ran against its own sandbox daemon (~/biorouter-runs/decision-gaps-live-20261107-0252/<tester>, evidence under evidence/). 62 of 62 legs passed; none failed.

Item Tester Leg What was observed Result
A swarmA P09-swarm-wire A1 Child 20261107_3 is status incomplete. Its resultLine reads: Stopped by monitor 'gives a dose' (set by this chat's agent): P 0.98 reached its threshold 0.60. Its question: "Has the assistant stated a specific numeric drug dose?". The run did not finish. The p… pass
A swarmA P15-swarm-spawn A2 (private parent refusal and acceptance) First spawn returned the refusal: Monitor 'off-task' could not be put on the subagent: This chat is private, so this decision cannot run on joey-1, which is a public model. ... Nothing was started. session list --subagents shows no child under that parent a… pass
A swarmA P15-swarm-spawn A4 (phase monitors) Children 20261107_7 and 20261107_8 both list phase-guard, setBy agent, setBySession 20261107_6, model ollama_decision/tev1:0.8b. pass
A swarmA P15-swarm-spawn A5 (inheritance with --subagents) All three children (20261107_10, _11, _12) list rm with inheritedFrom 20261107_9. The spawn reply line about monitors was not separately captured. pass
A swarmA P17-swarm-e2e A5 escalation Child _11 (the rm one) is incomplete with: Stopped by monitor 'rm' (the person's, from this chat): P 0.65 reached its threshold 0.60 ... The run did not finish. The other two children are incomplete with: Stopped because monitor 'rm', set by the person to wat… pass
A swarmA P11-workspace-tools A3 ownership The agent's list marks agent-guard as 'set by you' and person-guard as 'set by the person', with the line 'You may change or remove only the monitors set by you'. The remove call, naming both ids, returned: Error: Monitor person-guard on this subagent was s… pass
B swarmB P14-loop B1 The monitor fired at P 0.95 and the turn ended in 18 s. The notice read 'It also stopped 3 subagents this chat started'. All three children were incomplete with "Stopped because monitor 'fanout' stopped the chat that started this subagent." No sleep 300 was l… pass
B swarmB P10-cancel B2 (Stop via biorouter session cancel, keyed daemon) Foreground variant: it printed 'cancelled turn turn-5' and 'Also stopped 3 subagents this chat started.' Within 6 s all three children were incomplete with "Stopped because the person stopped the chat that started this subagent." No sleep 200 descendants were… pass
B swarmB P10-cancel B2 Stop-and-Send leaves children running HTTP 200 {cancelled:true, settled:true, continuation_lease:...} with no stopped_subagents field. After 5 s all 3 children were still running and 3 sleep 200 processes were alive. A later plain Stop then stopped them. Plain-cancel flows (Stop button, Esc, `ses… pass
B (escalati… swarmB P17-swarm-e2e B4 (A5 escalation) With clef-flash, subagent 3 stopped by 'rm' (P 0.93). Subagents 1 and 2 were incomplete with "Stopped because monitor 'rm', set by the person to watch every subagent of that chat, stopped subagent '...'". No looper processes were left under the daemon within … pass
B swarmB P11 workspace_close stops a child through the same step The tool answered 'Cancellation requested for background subagent session 20261107_35.' Child 1 ended incomplete with 'Subagent was cancelled before completion'. Its sleep process died, and the sibling stayed running with 1 sleep 200 left. Because this is a p… pass
C gates P11-workspace-tools C1 All as designed. Plan with research gate 0.9 accepted: gate owner agent, holds write and review. Redeclare at 0.0: 'already has a gate'. New phase gate 0.3: 'needs at least 0.50'. Plan dropping research: refused. start write: 'waits for phase "research"'s gat… pass
C gates P11-workspace-tools C2 joey-1 gate refused at declaration ('cannot be answered in this chat: This chat is private... joey-1, which is a public model'), and GET subagents shows plan null (nothing stored). tev1:0.8b gate accepted; complete passed (P(yes) 0.95); decisions?origin=gate … pass
C gates P13-routes (C) person gate routes with and without proof retry without proof: 403 with the PHASE_GATES_NEED_USER sentence ('is the user's decision... carried no proof'). With proof: 200, applied true, attempts 0, retriesGranted 1, phase back in_progress. PUT and DELETE without proof: same 403. PUT on V with joey-1:… pass
C gates P19-cli-rest (C) gates set/pass/skip/list and restart set without the key: refused with the NEED_USER sentence plus advice to pass --user-action-key-stdin. set with key: 'now has your gate... replaces the gate the agent set'; list shows 'set by you'. Complete research: passed (P(yes) 0.52 vs 0.30), a new gate de… pass
D latch P14-loop PUB (codex gpt-5.6-luna + baseten_decision/joey-1) Turn 1 stopped (P 0.93). 'Continue.' stopped again (P 0.86), which is the fix. After rearmBelow 0.2 the next Continue fired once more, with the notice 'stays quiet until P falls below 0.20'. The following Continue completed with no fire (P 0.75 still above th… pass
D latch P14-loop PRIV (versa_azure + ollama_decision/tev1:0.8b) Turn 1 stopped (P 0.90) and the second 'Continue.' stopped again (P 0.92). After rearmBelow 0.2 one more stop (P 0.86), then the next Continue completed with no fire (P 0.59). Decisions show 4 checks and the latch is true in sqlite. pass
1 changes a… midturn P14-loop (1) notify monitor added mid-turn watch shows "[notice] Monitor 'Delegation seen' fired (P 0.94, threshold 0.50)." after the workspace_watch calls and BEFORE the same turn's final summary text, then "checked 5 times (last P 0.96), fired" and [finished] stop. All 5 decision rows (sqlite) carry… pass
1 changes a… midturn P14-loop (1) edit the question mid-turn PUT 200. Decision rows of ONE turn (97804d5b): rows 1-5 carry the old question; every row from 11:04:18 on (12 rows, same turn) carries the new question. The turn was not restarted. Turn ended 'checked 17 times (last P 0.00)'. pass
1 (stop var… swarmB P14-loop (1) stop variant The same turn stopped at the next check, with the notice "Monitor 'Delegation seen' fired (P 0.97...)". The notice 'It also stopped 3 subagents' followed, and all three children were incomplete with the monitor-stop sentence. No sleep 120 was left within abou… pass
2 latch agent told why it stopped (PUB and PRIV) Each stop stored one user message with userVisible false and agentVisible true. It carries a block with the monitor name, its question and the reason, including the 'stays quiet until' wording. On PUB the export JSON shows them at mess… pass
3 no silent… midturn (a) unreadable monitors, onError pause No tool ran. Notice: "This chat's monitors could not be read (unknown field bogus, expected one of ...). At least one of them is set to pause when it cannot check, so the turn stops here. Save the chat's monitors again to replace them, then send a ..." then… pass
3 no silent… midturn (a) unreadable monitors, onError notify, and unreadable again while r… (i) one notice: "This chat's monitors could not be read (unknown field bogus ...), so none are checked this turn." Turn ran on. (ii) "This chat's monitors could not be read again (unknown field bogus ...); the ones read earlier are still checked." followe… pass
3 no silent… midturn (b)(c) narrow window, many checks 26 decision rows in one turn, one per loop iteration (about 3 s apart), context_bytes about 387-404 so the 2-entry window was honoured, final notice 'checked 26 times'. The recipe expected about 9 rows against 1 or 2 on main. pass
3 no silent… midturn (d) steer while a check could run watch shows '[user] [direct user message] Also mention the colour blue.'. The next check (11:11:59) was not dropped: its context_preview contains 'colour blue' (instr=66, no other row does), P rose 0.01 to 0.19. The monitor then fired at the end (P 0.68) and … pass
3 no silent… midturn (e) tiny budget, turn_end monitor not checked notice Notices: '⏳ Budget reached (34924 of 1000 tokens). Wrapping up with what I have.' then "Monitor 'Task done' was not checked at the end of this turn: the turn was being wound down because it reached its budget." 0 decision rows (no billed check past the budget… pass
4 saves P07-cli-monitors (a) fire below Add exit 0 ('P(yes) < 0.30 stops the turn'). list: 'P(yes) < 0.30 · once · stops the turn · after each iteration · stops again while it holds · an error pauses the turn · ... · ollama_decision/tev1:0.8b'. list --json: direction below, threshold 0.3, onError p… pass
4 saves P07-cli-monitors (b) checked save via daemon Exit 1: 'Error: Nothing was saved: monitor "X" could not run. This chat runs on a private model, so it becomes private at its first turn, and joey-1 is a public model a private chat cannot use...'. Never asked for a key. list unchanged ('no monitors'). no-s… pass
4 saves P07-cli-monitors (c) checked save on the local path On the private chat: exit 1 'Nothing was saved: monitor "X3" could not run. This chat is private, so this decision cannot run on joey-1, which is a public model...'; nothing stored (sqlite count 0). no-such-model locally: exit 1 with pull advice. Gap, not c… pass
4 saves P07-cli-monitors (d) --subagents Exit 0, 'It also watches the subagents this chat starts.' list shows 'also watches the subagents this chat starts'; list --json shows "applyToSubagents": true. --below and --subagents appear in add --help. pass
5 (honest n… billing covered by the late and abandoned sentences in P01 (1) and (2) The timeout row says 'It answered after 5.2 s, too late to use; its tokens are counted in Usage.' The abandoned row says 'The request was abandoned before joey-1 answered ... its answer was not used. It answered after 4.1 s; its tokens are counted in Usage.' … pass
5 crew P01-service (3) Crew Chat 1 (private, grant standing): tev1:0.8b answered (yes, p 0.96), and the decisions row is outcome decided. GET /decisions/models gave sessionUnavailableReason null, tev1 and clef-flash:latest usable, and all three Baseten rows refused with the new Crew sen… pass
5 install P13-routes GET install: state running, tev1 installed:false. POST llama3.1:8b gave 400 with sentence naming the two offered models. POST tev1:0.8b gave 202; 49 polls showed downloading with completedBytes rising 8.8MB to 1.52GB, then installed (the 'verifying' state was… pass
5 install P19-cli-rest List before showed only the 3 baseten models. Pull printed plain progress lines (pulling manifest, pulling model, writing manifest), 'Downloaded 1.5 GB. Checking that Ollama answers decisions with tev1:0.8b…', then 'Installed tev1:0.8b: Private, runs on this … pass
5 install P08-models-install Both real pulls of tev1:0.8b (1,524,372,929 bytes) completed, passed the decision-capability check and became usable by a private chat. Progress never went backward across the 49 polls. clef-flash:latest (12 GB) was not pulled. pass
6 saves P13-routes All refusals leave GET unchanged and begin 'Nothing was saved'. PRIV + joey-1: 422 'This chat runs on a private model ... joey-1 is a public model a private chat cannot use' (on an already-private chat: 'This chat is private, so this decision cannot run on jo… pass
7 latch latch survives a restart (PRIV) After the restart 'Continue.' completed with no stop, and the latch was still true. The first question edit gave P 0.48, below the threshold, so nothing fired and state reset to {criteria:{}}. A second edit to 'Is this conversation about fruits?' fired at P 0… pass
8 workflow P12-workflow-schema stop.yaml validates. on_error gives a parse error naming the unknown field with the allowed list. Top-level monitors gives 'monitors is not a top-level workflow field: monitors go under decision_settings.monitors.' treshold gives a parse error naming orch… pass
8 workflow P16 L1 (public, CLI) monitors lists reached-ten with default model baseten_decision/joey-1. subagents prints 'Plan: Count ... gate set by the workflow'. decisions shows a monitor row on joey-1 (P 0.97), and the turn ends with the stop notice. On codex the notice came only after a… pass
8 workflow P16 L2 (private) The monitor is listed with default model tev1:0.8b. Decision rows are on tev1:0.8b. The chat has privacy_tier private (versa_azure). The stop notice fired. With the numbers-to-30 prompt the model batched all 30 calls in one iteration, so the stop came at the … pass
8 workflow P16 L3 (refused) Printed 'The workflow was not started: monitor "reached-ten" could not run. This chat runs on a private model ... joey-1 is a public model a private chat cannot use. Choose a decision model that runs on this computer.' Exit 1. Session count was 11783 before… pass
8 workflow P16 L4 (HTTP) Keyless: 200, decision-settings holds tev1 default and reached-ten, subagents shows the plan. Threshold 1.5: 400 'The workflow was not started: ...the threshold must be a probability above 0 and at most 1, not 1.5.' Keyed with the public default provider: 403… pass
8 workflow P16 L6 (scheduler) stop-priv: run-now created session 20261107_11 with reached-ten on tev1 and a gated plan, plus decision rows. stop.yaml (public joey-1) run-now refused because the scheduled chat bound the private default; last_error held a sentence and no session was created… pass
8 workflow P16 L8 (slash) The reply is '/stopwf declares monitors or a phased plan, which apply only when the workflow starts its own chat. Start it from Workflows in the app or with biorouter run --workflow <abs path>; nothing was sent.' Finish reason stop, 0 tokens, 0 stored messa… pass
8 workflow P16 L9 (once only) The notice reads 'This chat already has messages. The monitors and phase plan this workflow declares were applied when the chat started and are not applied again, so the chat keeps them as they are now.' The monitor list stays empty afterward. The first run's… pass
8 workflow P16 L11 (--explain) Prints a Supervision section with the default model, the monitor line (threshold 0.70, stop, on error pause), the phase plan 'Count: 1 phase', the phase gate line, and the note that they apply once to the new chat. pass
8 workflow P16 L12 (capture) The generated workflow has decision_settings with defaultModel and both monitors, with the same fields as session monitors list (capmon on tev1, notify, 0.6), and an orchestration_plan skeleton with the gate (questions, threshold 0.7, max_attempts 2). Capture… pass
8 workflow P15 (8) sub-workflow monitors and orchestration_plan refusal Parent spawned checker as child 20261107_18. session monitors lists checker-watch with default model tev1:0.8b, and decisions shows 5 checks on it. With the orchestration_plan checker: 'Could not spawn subworkflow "checker": subworkflow checker declares an … pass
9 billing P01-service (1) late answer The CLI printed 'waiting up to 30 s for 1 decision request(s) that answered late, so their usage is counted', then 'Error: joey-1 did not answer within 2 s. Nothing was decided.', and exited 1 after 7 s wall (the late answer arrived at 5.2 s, plus startup). T… pass
9 billing P01-service (2) abandoned answer curl exited 28 (timeout). At 3 s no new row had appeared. By 9 s a new row existed: error, 117/1, 'The request was abandoned before joey-1 answered (the turn was stopped or the caller went away), so its answer was not used. It answered after 4.1 s; its tokens… pass
9 billing P02-adapters (A) malformed with --session (mangle) and drop-answers Mangle: exit 1, 'Error: ... returned an answer that could not be read: question ms is a noul question, but the answer is mangled'. The row is outcome error, 117/1, matching the proxy usage. token_events gained exactly one decision: row (count 3 to 4… pass
9 billing P02-adapters (A) without --session stderr shows "usage: 1 request on baseten_decision/joey-1: 117 input and 1 output tokens, less than $0.01. Without --session this is not recorded in Biorouter's usage." The run then exits 1 with the unreadable-answer error. The proxy usage was 117/1, which ma… pass
9 billing P02-adapters (E) local mangled answer on a versa chat exit 1, 'tev1:0.8b returned an answer that could not be read: question ms is a noul question, but the answer is mangled'. The decisions row is error 117/0, and the proxy usage was 117/0. token_events has one decision row (ollama_decision, tev1:0.8b, 117… pass
P20 slider,… desktop P20-desktop-monitors Slider has 21 stops (aria 0..20). Clicking the left end reads 0.01. Clicking back reads 0.30. 'Fires when' offers 'At or above / Below' with Below selected for Banana. 'If a check fails' offers 'Notify me / Pause the turn' with Pause selected. Rows read 'P(ye… pass
P20 CLI mon… desktop P20-desktop-monitors Stored record equals the GET before (equal-as-json True). Banana shows 0.30 and Below. The choice monitor row reads 'P(spms) < 0.20' (it names the option, as designed). Caveat: the comparison was made from the sessions.db row as parsed JSON, not as a byte-for… pass
P20 install… desktop P20-desktop-monitors The panel showed 'This chat is private, so only a decision model on the computer Biorouter runs on may read it, and none is installed.' with the Install button, using 'the computer Biorouter runs on' wording in the browser. After the click it showed 'Download… pass
P20 checked… desktop P20-desktop-monitors The dialog showed the 'Ollama not running at http://127.0.0.1:9/' state with Get Ollama and Check again. Saving the new monitor showed 'Nothing was saved: monitor "Pingtest" could not run. No decision model this private chat may use is available: only a decis… pass
P20 child c… desktop P20-desktop-monitors The DB holds rm-guard inheritedFrom=parent and agent-guard setBy=agent. The dialog rows show 'From the chat that started this subagent' and 'Set by the agent'. The 'Also watch the subagents this chat starts' checkbox is absent in the editor on the child. It i… pass
P21 gate ch… desktop P21-desktop-gates-workflows Chip reads 'Gate blocked: needs you'. Write, Review and Notes read 'Waits for Research's gate'. The dialog shows 'Set by the agent', 'Blocked: needs you', the question and 'Every question must reach P(yes) 0.90. 1 of 1 attempt used'. Show history opens the De… pass
P21 Edit ga… desktop P21-desktop-gates-workflows The slider reads 0.30 with the hint 'Below 0.50 the gate passes answers the decision model leans against. That is your call to make.' Save succeeded. The dialog then shows 'Set by you', 'Every question must reach P(yes) 0.30. 0 of 3 attempts used' and 'Phase … pass
P21 workflo… desktop P21-desktop-gates-workflows The warning modal shows the Supervision panel: default model tev1:0.8b · Private; monitors 'reached-ten · chat default · stops the turn'; Phase plan 'Count', 1 phase, gate with 1 question at P(yes) >= 0.70, 2 attempts; 'The agent cannot change, skip or remove… pass
B3 browser … desktop P10-cancel POST /agent/cancel returned 200. The daemon log reads 'Stopping the chat also stopped its subagents ... subagents: 3'. Loop-safety 'cancelled' lines for all three children appear. Each child chat ends with 'Stopped because the person stopped the chat that sta… pass

Completeness check (an independent critic read the results and evidence):

  • A: covered. Spawn-time monitors on subagents were exercised end to end: stop of a child, private refusal, phase monitors, inheritance, escalation and ownership, plus the desktop badges.
  • B: partly. Stopping the orchestrator stops its children for a monitor stop, a plain Stop (CLI and browser) and workspace_close. Two gaps remain: Stop-and-Send leaves children running, and children that ran sleep 300 as background shell jobs left orphaned OS processes, seen by swarmB and desktop.
  • C: covered. Phase gates were tested through the agent tools, the HTTP routes with and without the proof, the CLI (set, pass, skip, list, restart persistence) and the desktop (chip, held lines, dialog, Pass, Edit gate, history).
  • D: covered. The latch was shown fixed on both a public and a private chat. The monitor stops again on Continue, one more fire follows after rearmBelow, and the next Continue completes with the latch held.
  • 1: covered. A notify monitor added mid-turn fired in the same turn before the summary. A stop monitor added mid-swarm stopped the turn. An edited question applied to later rows of the same turn.
  • 2: covered. Each stop stored a hidden message the agent can see, with the monitor name, its question and the reason. The agent explained the stop on both a public and a private chat.
  • 3: covered. No-silent-skip legs all passed: unreadable monitors (pause, notify, and unreadable again while running), a check at every iteration with a narrow window, a steer during a check, and a not-checked notice at a tiny budget.
  • 4: partly. CLI monitors were tested for below, rearm validation, on-error, subagents, and the checked save through the daemon. One gap remains: without a daemon the local save does not know the bound tier, so a chat bound to a private model but with no turn yet stores a public model (joey-1).
  • 5: partly. The install routes, CLI pull and desktop install panel were tested with a real 1.5 GB tev1 install. The honest no-answer text and the local-model path were shown. Crew was tested only against a hand-written registry, not a real grant.
  • 6: covered. Checked saves refused bad models with 'Nothing was saved' on the routes, in the CLI and in the desktop dialog. The failure cases covered were a private chat with a public model, an unknown model, and Ollama down. An unproven PUT was refused with 403.
  • 7: covered. The latch survived a daemon restart on a private chat. The latch stayed true, the next Continue did not stop, and editing the question reset it and re-latched with a new fingerprint.
  • 8: covered. The workflow door was tested for validate, run (public and private), refusal, HTTP with and without the proof, scheduler, slash, resume, --explain, capture, and sub-workflows. The desktop Supervision panel and the YAML-kept-on-edit behaviour were also shown.
  • 9: covered. Late and abandoned answers were billed exactly once. The usage rows matched the proxy's 117/1, token_events had one decision row, updated_at did not move, and the honest sentences appeared. Malformed and dropped answers, the no-session note and a local mangled answer also counted correctly.

Re-test after the review fixes

The legs the later fixes touched were run again on the final binaries (~/biorouter-runs/decision-gaps-live-20261107-retest/), all passing:

  • B: a keyed biorouter session cancel stopped 3 children and said so; on a keyless daemon a public parent's Stop stopped and listed its children; a private parent's Stop with only the secret was refused (403) and stopped nothing; the same with a private caller stopped all three.
  • 3: a turn ending on its action limit (BIOROUTER_MAX_TURNS=3) printed the limit message, then "Monitor 'End check' was not checked at the end of this turn: the turn ended because it reached its action limit.", with no decision row; the tiny-budget leg names the monitor (a duplicate notice it showed was fixed in 3bb9956e9).
  • 6 / 4: with no daemon, adding a joey-1 monitor to a chat bound to versa_azure before its first turn exits 1 with the private-model sentence and stores nothing; the same on a codex chat succeeds. The daemon path got the same fallback for a chat it does not hold (3bb9956e9, covered by a route test).
  • 9: an on-time answer is billed exactly once (one row, tokens equal to the proxy's usage, one token_events row); late, abandoned and malformed answers still are.
  • 1: a monitor added mid-turn fired in the same turn; an edited question applied from the next check.

Known limitations

  • A subagent's background shell job (a command it started with the shell's background mode) keeps running after the subagent is stopped; foreground commands are stopped. Unchanged by this PR.
  • A turn that ends by an abort (the repetition guard, a provider failure, a context overflow) still skips its turn_end monitors without a notice; the action-limit, tool-call-limit, stall and budget exits now say so. Documented in docs/agent-loop/decision-models.md.
  • The Crew leg ran against a simulated Crew grant record on a sandbox daemon, not a real grant on leo, to avoid disturbing the Crew session working on leo.
  • workspace_monitor is offered even when delegation is off (about 600 tokens of tool schema per chat). The branch's total growth of the workspace tool schemas is about 1,400 tokens after a trim; hiding workspace_monitor without delegation would be a small follow-up.

How it was built and checked

21 work packages, each implemented on Opus and reviewed by an independent agent, with fix-ups for every finding; then a whole-branch adversarial review (10 lenses, 3-vote verification, 2 rounds) and parallel live testers, each with its own sandbox daemon and port. Chat models in the live tests were versa_azure (Private) and the codex coding-agent provider (Public); decision models were baseten_decision/joey-1 and local Ollama tev1:0.8b / clef-flash:latest.

🤖 Generated with Claude Code

…models in Crew

- A billed answer that cannot be read keeps its usage: decision rows carry the
  tokens and one token_events row is written per decision (item 9).
- A late answer after a timeout, or one whose caller went away, is settled
  exactly once in the background; the process waits for in-flight ones at exit.
- Threshold::Below fires below a level (item 4).
- check_model_use / check_settings refuse at save time a monitor whose model or
  tier cannot run for the chat (item 6).
- Crew chats may use decision models that run on this computer; hosted ones are
  still refused, with a sentence that says why (item 5).
- Adapters keep the usage of answers they cannot parse; the catalog says when
  Ollama is not answering; an installer pulls a local decision model.
…ir latch

- Stop monitors stop again on every turn while their answer holds; rearmBelow /
  rearmAbove give the old stop-once behaviour (item D).
- Settings are re-read at every check point, so a change applies mid-turn
  (item 1); the stop note is also given to the agent (item 2).
- Unreadable settings, an empty or unchanged window, the steer grace and a tiny
  budget either check or say so; onError pause stops the turn (item 3).
- The latch is stored on the session and survives a restart (item 7).
- Below monitors run through the reply loop (item 4); a monitor stop also stops
  the chat's subagents with a bounded wait (item B).
…gents

- A gated phase cannot be skipped, dropped from a re-declared plan, or have its
  gate loosened or replaced by the agent; holds and depends_on are enforced at
  start and spawn; the person passes, skips or retries a gate (item C).
- workspace_monitor lets the agent put monitors on its subagents, with the
  first-crossing disclosure for a private chat writing into a public child;
  the agent cannot remove a monitor the person set (item A).
- workspace_close stops a child through the same step as Stop (item B).
…onitors

- One function stops every subagent a chat started; the person's Stop, a monitor
  stop and workspace_close use it, while Stop-and-Send leaves children running
  (item B). Each stopped child records why.
- Monitors can be set at spawn, come from a plan's phase, or be inherited from a
  parent monitor that also watches subagents; a child's monitor stop reaches the
  parent's result; children use the parent's decision service (item A).
- A sub-workflow's monitors supervise its child (item 8).
- The CLI waits for late decision answers before exiting (item 9).
… a gated plan

- decision_settings and orchestration_plan blocks in workflow YAML, validated
  with clear sentences (item 8).
- Every door that starts a workflow (biorouter run, POST /agent/start,
  schedules, slash commands, sub-workflows) seeds supervision once, refuses
  before creating a session when it cannot run, and --explain prints it.
- Creating a workflow from a chat captures its supervision.
… routes

- Saves answer 422 when a monitor is valid but cannot run for the chat, 400 when
  malformed; 403 stays for reach and proof refusals (item 6).
- Install routes for a local decision model (item 5) and person routes to pass,
  skip, retry or set a phase gate (item C), all behind session_reach.
- OpenAPI spec and TypeScript client regenerated.
…ates, decision pull

- biorouter session monitors: --below, --rearm-above, --on-error pause,
  --subagents, owner display, checked saves (items 4, 6, A, D).
- biorouter session gates set/pass/skip/retry/list (item C).
- biorouter models decision pull installs a local decision model (item 5).
- Workspace parity rows and TUI subagent lines show gates and monitor stops.
…e gates

- Monitors dialog: thresholds from 0.01, Fires when Below, pause the turn when a
  check fails, owner badges, also watch subagents, 422 sentences shown (items 4,
  6, A); an install panel for a local decision model (item 5).
- Phase gate dialog, needs-you chip and held lines in the summary rail (item C).
- The workflow warning shows the supervision a workflow will set, and the
  workflow editor keeps its blocks (item 8).
Describes what the code now does: stop monitors that stop again and their
persisted latch, mid-turn changes, the stop note to the agent, the paths that
no longer skip silently, fire-below thresholds and onError pause, local
decision model install and Crew use, checked saves, workflow supervision
blocks, billing of unreadable and late answers, agent monitors on subagents,
stops that reach the swarm, and gates that bind the plan.
- Monitor re-arm uses the same epsilon as firing, so a value on the threshold
  neither fires nor re-arms twice.
- A subagent stopped during a later turn, or between finishing and publishing
  its result, records why it was stopped.
- The Crew refusal no longer says no local model is installed when the
  configured hosted provider is what excludes it.
- Misplaced supervision keys are refused at every door that takes a workflow
  as JSON (deeplinks, the workflow routes), not only when read from a file.
- The install test's idle bound no longer depends on wall-clock headroom.
- Desktop: the gate row state agrees with the summary rail (person's pass or
  skip, needs you), and a monitor's re-arm level can be edited or cleared so a
  CLI-made monitor is not stuck with a now-invalid re-arm after the threshold
  moves.
Review round 2:
- A plain Stop on /agent/cancel stops and names only the subagents the caller
  could reach on their own; a child raised to private under a public parent
  keeps running for a secret-only caller and its id is not returned.
- An answer that arrives on time is stored and billed in a task the caller's
  cancellation cannot drop, so a Stop or a gone HTTP client mid-write no longer
  loses its row or its token_events row.
- Workflow capture keeps multi-line supervision fields intact; a chat seeded by
  one workflow is not reported as seeded by another.
- Desktop: editing a phase gate keeps its decision-model adapter; a passing
  "below" check is not labelled "Below threshold"; decision settings refresh
  when an agent or another door changes them.

Follow-ups:
- Turns that end on their action limit, tool-call limit, stall stop or budget
  stop name the turn_end monitors they did not check (item 3).
- With no daemon, `biorouter session monitors add` checks a save against the
  chat's bound model, so a private-bound chat refuses a hosted monitor before
  its first turn (item 6).
- Workspace tool descriptions trimmed: the branch's growth of the workspace
  tool schemas fell from about 3,500 to about 1,400 tokens, which also brings
  the ollama_diagnostics prompt-size test back under its bound.
- clippy too_many_lines: helpers extracted in agent.rs, orchestration_plan.rs,
  workspace/turn.rs and subagent_tool.rs (no behaviour change).
- Docs: the forced-exit notices and the remaining abort gap.
…nce per turn

- The daemon's save check (decision-settings, decision-chat and phase-gate
  PUTs) asks the chat's agent for its bound tier and, when the daemon holds no
  agent for the chat or the agent has no model yet, reads the provider named
  on the session row. A chat bound to a private model before its first turn
  now refuses a hosted monitor or gate right after a restart too (item 6;
  found by the live re-test).
- A turn names each unchecked turn_end monitor at most once, so a budget stop
  after a wound-down wrap-up no longer repeats the notice (item 3).
@Broccolito
Broccolito marked this pull request as ready for review November 7, 2026 13:29
…iff)

CI on Linux and Windows aborted biorouter-server's lib and biorouterd test
targets with a tokio worker stack overflow in
a_steer_in_the_turn_prologue_is_accepted_and_the_agent_stays_pinned: the
branch's monitor awaits had pushed the turn's stack need from about 1.9 MiB
(origin/main) to just under the 2 MiB worker limit.

In a debug build `Box::pin(self.helper(..)).await` still builds the helper's
future in the caller's frame, so `boxed_off_frame(|| self.helper(..))` now
builds it in its own frame. Used for the monitor points, forced-exit notices
and supervision step the branch added, a few existing reply-loop awaits, the
reply prologue, and the workspace turn runner (run_turn, run_turn_body,
open_reply_stream, the child-turn bookkeeping). No behaviour change; steer
arms are untouched.

Measured on macOS debug: the test's minimum worker stack fell from 2026-2041
KiB to 1023-1030 KiB; Agent::reply's future from 23,576 to 1,248 bytes and the
spawned turn from 158,248 to 3,304. A new test pins the reply, run_turn and
spawned futures at 8 KiB or less.
…on.rs

The privacy_capability census (compatible_is_the_only_function_that_compares_
two_affiliations) failed in CI: the Crew rule for decision models compared
ModelAffiliation::Local by hand in decision/service.rs, and its test gate did
the same in routes/decisions_tests.rs. The comparison now lives beside
`compatible` as `privacy::affiliation::runs_on_this_computer`, with a unit
test, and both sites call it. No behaviour change.
@Broccolito
Broccolito merged commit cf1bbbf into main Nov 7, 2026
18 checks passed
@Broccolito
Broccolito deleted the feat/decision-supervision branch November 7, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant