Files

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) and harnessOn for binds.
  • RUNNER_FILES drops bundle.mjs (the runner no longer imports it) and adds kinds.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 --pm refuses harness-off while plugins.harness is unset. At the re-pin the bus starts without --pm, Jason runs the plugins patch, then the PM launches.

Changes outside the owned list

  • claudeVersion moved out of the launcher into the claude-code module's version(), with row 68's precedence (MOSAIC_CLAUDE_BIN, else PATH claude) and its tests, now in harness.test.mjs. The manifest's piVersion and claudeVersion fields are replaced by one harnessVersion from the module the route took. Nothing in the tree reads either old field (grep over packages, scripts and the READMEs); old run manifests keep theirs.
  • Not changed, follow-up: packages/cli/systemd/mosaic-bus.service.in:16-17 still 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 no modules( 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, isAbsolute from node:path (the transcript check) and NAME from packages/plugins/src/kinds.mjs (the harness name pattern). kinds.mjs imports nothing and is in RUNNER_FILES, so the sandboxed runner can load the broker.
  • packages/plugins/tests/loader.test.mjs: each loaded map gains harness: [].
  • 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/routes POST, and the method: 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 sets plugins.harness so its runtime launch binds. usage and usage-runtime bind a transcript, and createUsage no longer takes dataRoot.
  • 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):

  1. runner.mjs:276 spreads session.harnessEnv last, so a module's env can shadow a core variable. The brief puts env in the module.
  2. launcher.mjs wraps levels() in call() but not the .includes and .join after it, so a module returning a non-array throws a raw TypeError; a preflight result with no code gives Refusal(undefined).
  3. adapter() and mounts() run after the workspace and session dirs exist; a throw there leaves them behind.
  4. mosaic-bus.service.in:16-17 still names claudeVersion. Already on the row 166 list above.
  5. Darkwing and Dewey, round 2: when the chief of staff sends a partial fallback through assistants, the write is refused, but with invalid-request instead of organization-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).