23 KiB
Row 65: harness, model and thinking from the instance vars, a fallback route, and the harness plug-in kind
Sage, 2026-10-19. Brief: docs/plans/2026-10-11_managed-session-config.md, section "Model routing
table", blob 0cdff87390 (0235fa9fd, queue rev 1950; round 1 was blob f3b06a1655). Issue #1558.
Reviewers: Darkwing and Dewey. Built in the detached worktree /mnt/storage/scratch/sage-r65.
Round 2 candidate 8d7c9e2f1 (8d7c9e2f14) on base 89ddb0225: 64 files, 2292 insertions, 336 deletions. candidate.diff is
git diff 89ddb0225 8d7c9e2f1; candidate-manifest.sha256 has the sha256 of each changed file
at the candidate. Round 1 was ccdbc2232 on d8336daef; d8336daef..89ddb0225 touches no manifest
path (it adds this packet, the brief re-pin, SESSIONS and queue revs 1945-1950).
public.diff in this directory is the packages/webui/src/public/ diff for Mos (decision 123).
What changed
Harness plug-in kind. kinds.mjs gains harness (interface 1, switch list). A module
exports adapter, levels, preflight, version, countsAccount, mounts, builtinWeb,
builtins, typedName, reliesOn, gateTimeout, bundle, env, turnEnd and transcript.
packages/plugins/harness/pi/ and claude-code/ hold what core used to branch on: the adapter
path, the thinking levels, the binary and credential preflight, the version probe, the sandbox
mounts, the builtins prompt line, the typed tool names, the gate timeout, the bundle files (the
pi extension or the Claude settings and MCP files), the adapter environment, how a turn ends and
where the session files live. Core reaches a module only through the loader.
Route. sessionModel(resolved, models, which) (bundle.mjs) reads harness, model and
thinking, or the three fallback. vars, from the instance. A missing one throws no-route
naming the var. The system file's execution.adapter and execution.model are never read for a
session; they stay for run-task.sh.
Launcher check order (launcher.mjs). Primary route, then the fallback route when
fallback.harness is set; no-role-contract; harness-off for either route not in
plugins.harness, so an off primary never falls back; the primary module through a fresh
loader (harness-refused names the module and the loader's code); its levels
(thinking-unsupported); its preflight. A failed preflight with no fallback refuses with the
preflight's code. With a fallback, the fallback module goes through the same steps, and a second
failure names both reasons. The account checks (only when the route module's countsAccount()
is true), the sandbox check, the capacity limits and mayLaunch all run on the route taken.
Any refusal of the fallback (preflight, harness-refused, harness-failed,
thinking-unsupported) ends with (primary <harness>: <reason>), so the reason it was tried
stays in the launch log.
The manifest records route (primary or fallback), routeReason (the primary's preflight
code, or null) and harnessVersion.
Thinking. pi's adapter passes --thinking <level>; Claude Code's passes --effort <level>.
Both read MOSAIC_THINKING. The levels each module applies:
| Harness | Binary the session runs | Flag | Levels |
|---|---|---|---|
| pi | 0.85.1, the repository's node_modules/.bin/pi (the sandbox mounts the repo's node_modules on the session's PATH; pi on the host PATH is 1.2.0 and isn't used) |
--thinking <level> |
off, minimal, low, medium, high, xhigh, max |
| claude-code | 2.1.302 (MOSAIC_CLAUDE_BIN or PATH claude) |
--effort <level> |
low, medium, high, xhigh, max |
The thinking var takes off, minimal, low, medium, high, xhigh and max. Nothing is remapped: a
level the route's harness doesn't apply refuses thinking-unsupported, at a write and at launch.
The live file uses high on five instances and medium on the coder, all claude-code, so nothing
live changes. adapters/README.md has the MOSAIC_THINKING row.
Business vars and writes. harness and fallback.harness take a module name;
fallback.model is a string; thinking and fallback.thinking take a level. The file layer
refuses a fallback with one or two of its three vars. checkRoutes (launch-settings.mjs) is shared
by routes.set, instances.set and limits.set: unknown-model for a fallback model outside
launch.models, then, only for a route whose harness or thinking changed, harness-off,
harness-refused (the module didn't load, so its levels are unknown) and thinking-unsupported.
A route the write doesn't touch isn't re-checked, so an earlier unswitched harness doesn't block
an unrelated save. setRoutes checks the primary and fallback models against launch.models
before the file's own validation, which would refuse an unknown primary model with no code, so
routes.set answers unknown-model for either (round 2, Dewey N1).
Routes verbs (mos-0840). routes reads {revision, harnesses, levels, models, instances};
routes.set takes exactly {revision, routes}, each route {harness, model, thinking, fallback} with fallback null or all three. Both are human-only broker actions outside
ORGANIZATION_KEYS, so organization.set and the chief of staff's change_organization can't
send a routes key. The writer logs the human as actor. The bus passes harness-off,
harness-refused and thinking-unsupported through as their own codes, and unknown-model
for routes.set only (the older verbs keep invalid-request).
The chief of staff still reaches one route. Under Q169 (Jason: "The COS and the user should have
the ability to (re)configure the assistant role and seat name"), organization.set takes
assistants.<instance>.vars, which replaces the assistant's whole vars, harness, thinking and
fallback.* included. Round 2 keeps that and names it in the brief (ruling (b), sage-1006,
blob 0cdff87390): the write goes through limits.set, runs the same checkRoutes, and logs
chief-of-staff.<instance> as actor. Every other instance's route is human-only.
Bus binds. A new run binds only on a harness module name (HARNESS_NAME, the loader's
pattern) that plugins.harness switches on now (harnessOn, read from the file at bind time);
otherwise invalid-harness. A run the store already holds binds again unchanged, so a switched
off harness doesn't strand a running session. The bind record takes transcript, the module's
session-files path, which the usage reader now uses instead of a harness-name branch.
Console (mos-0840). Settings gets a "Routing" section and loses the "Role models" fieldset
from Agent limits. One row per instance, six selects: harness, model, thinking, and the
fallback's three. Each select offers the switched-on harnesses, the model table's keys or the
levels the row's harness applies. A value the file holds that isn't offered stays selected with
its reason in the label ("not switched on", "not in the model table", "not applied by
claude-code"), so a save sends it and the bus names the refusal. A fallback with all three set to
none is cleared; a partial one is sent as it is and refused. Save posts only the changed rows to
POST /api/routes with the read's revision. Every refusal keeps the draft; nothing retries.
GET /api/bus/routes reads the table. packages/webui/src/bus.mjs is unchanged, so the
channel-proof sha256 pin holds.
Decisions taken in the build
- Write-time checks run on changed routes only; the model check runs on every model set. A
missing route var is a launch refusal (
no-route), not a write refusal, so the file can hold an instance that isn't launchable yet. - The bus runtime gains
harnessLevels()(each module in the tree, loaded once at start, on or off, so a write can check a level before the harness is switched on) andharnessOnfor binds. RUNNER_FILESdropsbundle.mjs(the runner no longer imports it) and addskinds.mjs.- The runner's turn check is by
turnEnd(): pi's "marker" needs the marker file, and the message reads "(no turn marker)". Claude Code's is "exit". - A usage end after a begin with no transcript reads
baseline-unknown. bus start --pmrefusesharness-offwhileplugins.harnessis unset. At the re-pin the bus starts without--pm, Jason runs the plugins patch, then the PM launches.
Changes outside the owned list
claudeVersionmoved out of the launcher into the claude-code module'sversion(), with row 68's precedence (MOSAIC_CLAUDE_BIN, else PATHclaude) and its tests, now inharness.test.mjs. The manifest'spiVersionandclaudeVersionfields are replaced by oneharnessVersionfrom the module the route took. Nothing in the tree reads either old field (grep overpackages,scriptsand the READMEs); old run manifests keep theirs.- Not changed, follow-up:
packages/cli/systemd/mosaic-bus.service.in:16-17still says the pinned binary serves "the manifest's claudeVersion". It's a comment, but row 166 changes this unit and Mos rechecks its quoting before that lands, so I left the file alone. One-word fix after row 166 lands. packages/cli/tests/launch-limit-checks.test.mjs(row 125's file), the REQ-LIMIT-4 test. It asserted the launcher never imports the plug-in loader. The launcher now loads its harness through the loader, so the assertion is narrowed: the launcher's only.load(call is{ harness: [m.harness] }, and nomodules(call names a kind other than harness. A limit check still can't load in the launcher. This is the one guarantee in another row's test that got weaker, and it's the reviewers' call whether the narrowed form holds.packages/bus/tests/project-runtime.test.mjs, the broker import-closure test: the broker gains two imports,isAbsolutefromnode:path(thetranscriptcheck) andNAMEfrompackages/plugins/src/kinds.mjs(the harness name pattern). kinds.mjs imports nothing and is inRUNNER_FILES, so the sandboxed runner can load the broker.packages/plugins/tests/loader.test.mjs: eachloadedmap gainsharness: [].packages/webui/tests/settings-form.test.mjs: the Agent limits form has no selects now, and the save no longer sets an instance model.shell.test.mjs: the write list gains the/api/routesPOST, and themethod:count goes from 6 to 7.- Bus test fixtures (
chief-of-staff,credential-status,daily-quota-runtime,end-launch,limits-runtime,process,tasks,tester,usage-runtime): each business fixture setsplugins.harnessso its runtime launch binds.usageandusage-runtimebind atranscript, andcreateUsageno longer takesdataRoot. - READMEs beyond the brief's list: bus, cli, seat and webui, for the same changes.
Round 2
Round 1 drew changes from both reviewers: Dewey, comment 29136 (rev 1947), and Darkwing, comment 29137 (rev 1948). Each finding and what round 2 does about it:
| Finding | Round 2 |
|---|---|
Dewey B1, Darkwing F1: the chief can change the assistant's harness, thinking and fallback through assistants.<i>.vars |
Ruling (b), sage-1006: Q169 stands and the brief names the assistant as the one exception (0235fa9fd). The write already runs checkRoutes; the new chief-of-staff case proves it through the real bus, refusals and a passing save. Comments corrected in broker.mjs, launch-settings.mjs, routes-verb.test.mjs, the bus and business READMEs and this file. serve.mjs's comment is about the routes.set verb, which the chief's tool still can't reach, so it stays. |
Dewey B2, Darkwing F3: no thinking-unsupported or dropped-fallback-columns mutant |
m8a, m8b and m9 below. |
| Dewey B3, Darkwing F2: no test reads a thinking flag from a run | harness.test runs both real adapters against recording stand-in binaries (Tests table). |
Dewey N1: unknown-model can't reach the Console |
The broker passes unknown-model through for routes.set, and setRoutes checks the primary model before the file's uncoded refusal. routes-verb asserts both. The webui ROUTE_REASONS entry is now reachable. |
Dewey N2: a fallback refused at harnessOf drops the primary's reason |
launcher.mjs appends (primary <harness>: <reason>) to that refusal; launch-routes asserts it for thinking-unsupported and harness-refused. |
REQ-LIMIT-4: both reviewers accept the narrowed launch-limit-checks assertion. Darkwing's
optional tightening, pinning the single createLoader import, isn't in this round.
Darkwing's round 1 runs on ccdbc2232, by path only: ~/darkwing-scratch/r65/singles/ (33
changed test files singly, 311 pass, 0 fail), mut/ (their m8-m11, all killed; m11 only
because the table never renders) and probe/ (the F1 business-layer probe).
Follow-ups, no change in this row (Darkwing's notes):
runner.mjs:276spreadssession.harnessEnvlast, so a module's env can shadow a core variable. The brief puts env in the module.launcher.mjswrapslevels()incall()but not the.includesand.joinafter it, so a module returning a non-array throws a raw TypeError; a preflight result with nocodegivesRefusal(undefined).adapter()andmounts()run after the workspace and session dirs exist; a throw there leaves them behind.mosaic-bus.service.in:16-17still namesclaudeVersion. Already on the row 166 list above.- Darkwing and Dewey, round 2: when the chief of staff sends a partial fallback through
assistants, the write is refused, but withinvalid-requestinstead oforganization-partial-fallback. The refusal holds; only the code is less exact.
Tests
| Test file | Covers |
|---|---|
packages/cli/tests/launch-routes.test.mjs (new, 9) |
primary taken when it passes; fallback on a failed preflight with limits on the fallback; no fallback refuses with the preflight code; both failing names both; account on a counting fallback; no-route per var with execution keys never used; harness-off primary or fallback; harness-refused and harness-failed; thinking-unsupported on primary and on a fallback taken; a fallback refused at thinking-unsupported or harness-refused names the primary's failure (round 2) |
packages/cli/tests/launcher.test.mjs (row 65 case) |
a real sandboxed launch on a fixture harness module no core file names, switched on in a test organization file: the primary fails preflight, the fallback runs, the manifest records route and reason |
packages/plugins/tests/harness.test.mjs (new) |
both modules load with every export; per-module answers; claude-code preflight codes; version probes; env and bundle files; the grep test, with the brief's exceptions listed and each still naming a harness; round 2: the real adapters/pi/adapter.sh and adapters/claude/adapter.sh run against stand-in pi and claude binaries that record their argv, with each module's env() for thinking xhigh and low, and the recorded argv holds --thinking <level> or --effort <level> exactly once |
packages/bus/tests/routes-verb.test.mjs (new, 4) |
human-only for every non-human cap including the chief; routes not in ORGANIZATION_KEYS; harnessOn binds; harnessLevels; the runtime write path and each refusal leaving the bytes and the log unchanged; round 2: unknown-model for a primary or fallback model the table lacks |
packages/bus/tests/chief-of-staff.test.mjs (round 2 case) |
the chief's assistant write through the real bus: harness-off for an off harness, primary or full fallback; thinking-unsupported for claude-code at off; a partial fallback refused (invalid-request, the file layer's uncoded refusal); each with the bytes and the log unchanged. A valid route with a whole fallback saves, the vars equal the route, and the log's actor is chief-of-staff.chief (Dewey's suggested passing case) |
packages/business/tests/launch-settings.test.mjs, instance-settings.test.mjs, vars.test.mjs |
checkRoutes codes through each writer; partial fallback at the file layer |
packages/webui/tests/routes.test.mjs (new, 5) |
GET and POST routes, status codes, receipts, the identity log line |
packages/webui/tests/routes-browser.test.mjs (new, 1) |
the table lists every instance with six values; read-only without send; a full save read back from the file; partial fallback, thinking-unsupported, revision-conflict and harness-off each refused with the draft kept; "Nothing changed." posts nothing |
The chief's tool refusing a routes key is covered at the bus (organization.set with the
chief's cap); there's no separate chief-tool test.
Mutants
/mnt/storage/scratch/sage-r65-out/mutants.py, run in a second worktree
(/mnt/storage/scratch/sage-r65m, checked out at 8d7c9e2f1 and the same files as the gated
tree by diff -rq, before and after the run), so the gated tree was never edited. Each mutant
replaces one anchor that appears exactly once, runs the tests named for it, and restores the
file from a checked backup. Round 2 ran after the gate, in the slot: the control run of the
union of the tests passed 87/87 first (round 1: 71/71, 8 mutants).
| Mutant | Edit | Killed by |
|---|---|---|
| m1-no-route | sessionModel returns the system file's execution.adapter instead of throwing no-route |
launch-routes no-route test; bundle.test sessionModel |
| m2-harness-off-launch | launcher skips the harness-off check |
launch-routes harness-off primary or fallback |
| m3-harness-off-write | checkRoutes skips harness-off |
5 tests: launch-settings (3, Q169 assistants among them), routes-verb runtime, webui routes |
| m4a-partial-fallback-file | vars.mjs skips the partial-fallback check | vars.test types (the partial cases at lines 73-74) |
| m4b-partial-fallback-routes | setRoutes skips the partial-fallback check |
launch-settings setRoutes; routes-verb runtime |
| m5-fallback-on-pass | a passing primary preflight is treated as failed when a fallback is set | launch-routes "a passing primary is the route" |
| m6-harness-name-in-core | sandbox.mjs gains DEFAULT_HARNESS = "pi" |
harness.test grep test |
| m7-routes-in-org-keys | routes added to ORGANIZATION_KEYS |
routes-verb human-only test |
| m8a-thinking-write | checkRoutes skips thinking-unsupported |
launch-settings setRoutes; routes-verb runtime; the chief-of-staff Q169 route case |
| m8b-thinking-launch | launcher skips thinking-unsupported |
launch-routes thinking test |
| m9-fallback-cells | the routing table drops its three fallback cells (public/bus.js, mutant tree only) |
routes-browser, on the cells' deep-equal |
| m10a-pi-thinking-flag | pi's adapter never sets --thinking |
harness.test adapter argv test |
| m10b-claude-effort | Claude Code's adapter drops --effort |
harness.test adapter argv test |
| m11a-chief-skips-checks | limits.set skips checkRoutes when the actor is a role (<definition>.<instance>) |
the chief-of-staff Q169 route case, on a missing refusal |
| m11b-chief-refused-route | the chief's assistant write is refused whenever it sets harness |
the chief-of-staff Q169 route case, on the passing save |
| m12-unknown-model-masked | the broker drops the routes.set unknown-model pass-through |
routes-verb runtime |
| m13-unknown-primary-uncoded | setRoutes checks only the fallback model |
launch-settings setRoutes; routes-verb runtime |
| m14-fallback-refusal-unnamed | a fallback refused at harnessOf no longer names the primary's failure |
launch-routes thinking test |
18 of 18 killed, each by an assertion failure in the named test, not a load error. Round 2
logs are in /mnt/storage/scratch/sage-r65-out/mutants/ (console mutants-r2.console.txt),
round 1's in mutants-r1/. Darkwing's m11 shape (dropping the fallback ROUTE_CELLS, killed
only because the table never renders) is left out on purpose; m9 is the column check.
Gate
Round 2: /mnt/storage/scratch/sage-r65-out/gate2.sh (gate.sh with the output dir g2/) on the
committed candidate 8d7c9e2f1, dirty 0. GATE_PASSED, 25 steps, each rc=0. 2514 node tests
(2358 package, 156 in test-queue) and 422 script selftests, 0 failed, summed with awk over
g2.console.txt. The same one webui skip. pkg-bus is 420, one more than round 1 (the new
chief-of-staff case); the other brief suites match round 1: pkg-plugins 111, pkg-business 148,
pkg-harness 123, pkg-seat 151, pkg-cli 344, pkg-webui 237, and every test-*.sh. Compose project
sage-r65; its network was removed on exit and no container was left.
Round 1: /mnt/storage/scratch/sage-r65-out/gate.sh, from the 2026-10-14 template, on d8336daef plus
the 64 changed files, the tree committed as ccdbc2232 with nothing else changed (git diff HEAD --quiet after the commit): every package suite except queue, then every scripts/test-*.sh.
GATE_PASSED. 25 steps, each rc=0. 2513 node tests (2357 package, 156 in test-queue) and 422
script selftests, 0 failed, summed with awk over g1.console.txt. One skip, the existing webui
esbuild rebuild test (pinned esbuild not installed), which this row doesn't touch. The brief's
suites: pkg-plugins 111, pkg-bus 419, pkg-business 148, pkg-harness 123, pkg-seat 151, pkg-cli
344, pkg-webui 237, and every test-*.sh. Compose project sage-r65; its network was removed on
exit and no container was left.
Landing
At landing Sage sends Jason, through Mos, the patch for the live business file:
mosaic plugins set adding "harness": ["claude-code"]. Nothing goes live before a re-pin on
Mos's go. Until that patch is in, a new launch refuses harness-off.
Landed 2026-10-19 as c216f47db, 8d7c9e2f1 cherry-picked onto 866f3c5a8 with the round 2 message.
Approvals: Darkwing 29144 (rev 1953) and Dewey 29145 (rev 1954), both on manifest 8df15e39.
Between 89ddb0225 and 866f3c5a8 only this packet, the queue files and SESSIONS changed, none of
them a candidate path. The landed commit's path list has the same sha256 as the candidate's, all
64 manifest hashes check against the landed tree, and the public/ +/- digest is a91cf866, the one
Mos cleared (mos-0841 condition 3).
Gate g3 (/mnt/storage/scratch/sage-r65-out/gate3.sh) on c216f47db, dirty 0: GATE_PASSED, 25
steps, each rc=0. 2514 node tests (2358 package, 156 in test-queue) and 422 script selftests, 0
failed, summed with awk over g3.console.txt, the same counts as g2. The same one webui skip.
Network sage-r65_default removed, no container left.
The mos-0840 view is the Routing section of the Console's Settings page (<h2 id="routes-title">Routing</h2> in packages/webui/src/public/bus.js), which replaces the Role
models fieldset.
The patch for Jason, sent through Mos: read the revision with mosaic plugins --business mosaic-stack --json, then run mosaic plugins set --business mosaic-stack --file PATCH with
{"revision": "<that revision>", "plugins": {"harness": ["claude-code"]}}. A kind the patch
doesn't name keeps its setting, so this adds harness to whatever plugins holds. It works only
on a stack pinned past c216f47db: an older setter refuses the harness kind as unknown. A new
bind reads plugins.harness from the file as it stands, so no bus restart is needed for it.
After that Sage runs one managed coder session and attaches the manifest (module, route,
counted model, thinking).