sage: row 65 packet, candidate ccdbc2232 manifest, diff, public/ diff, build notes, gate and mutants (#1558)
This commit is contained in:
1 parent
d8336daef2
commit
1d5897024d
4 files changed
+4566
No files matched your search
@@ -0,0 +1,186 @@
|
||||
# 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 f3b06a165586 (841e0b0a2, queue rev 1944; lead decision 151 and mos-0840 folded in). Issue #1558.
|
||||
Reviewers: Darkwing and Dewey. Base d8336daef. Built in the detached worktree
|
||||
`/mnt/storage/scratch/sage-r65`.
|
||||
|
||||
Candidate **ccdbc2232** (ccdbc2232ddd6f879356bbbff9c2805797c9be3e) on d8336daef: 64 files,
|
||||
2189 insertions, 336 deletions. `candidate.diff` is `git diff d8336daef ccdbc2232`;
|
||||
`candidate-manifest.sha256` has the sha256 of each changed file at the candidate.
|
||||
|
||||
`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.
|
||||
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` (the file's own check refuses a bad primary model first, as `invalid-request` at
|
||||
the bus), 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.
|
||||
|
||||
**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
|
||||
reach them. The writer logs the human as actor. The bus passes `harness-off`, `harness-refused`
|
||||
and `thinking-unsupported` through as their own codes.
|
||||
|
||||
**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.
|
||||
|
||||
## 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 |
|
||||
| `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 adapters' `--thinking` and `--effort`; the grep test, with the brief's exceptions listed and each still naming a harness |
|
||||
| `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 |
|
||||
| `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 routes 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`, the same files as the gated tree by `diff -rq`), 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. The control run of the union of those
|
||||
tests passed 71/71 first.
|
||||
|
||||
| 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 |
|
||||
|
||||
8 of 8 killed, each by an assertion failure in the named test, not a load error. Logs are in
|
||||
`/mnt/storage/scratch/sage-r65-out/mutants/`.
|
||||
|
||||
## Gate
|
||||
|
||||
`/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`.
|
||||
@@ -0,0 +1,64 @@
|
||||
fc5425680bfe5ce48bb41ba5aabe817c5a20ce3ebd4b74c5c03b902ed1b27f26 adapters/README.md
|
||||
40df61ad93d8109aa4348e32676649e94eca7e2ebc140dfb60b66379c93eb478 adapters/claude/adapter.sh
|
||||
1eda0066a2dee2ffdcde9db5c9a38774f8c452c73383b5f1995e0b97977c62f7 adapters/pi/adapter.sh
|
||||
f1e2ddfaa376b968ffce02fc77e3a902a8ab094c216dec6b7f76ae9efb1ff5ff packages/bus/README.md
|
||||
5888dc3c02f53a1cee9ade1ded0092985e8ca725bc4f11b99fb8ae2b2b9d9375 packages/bus/src/broker.mjs
|
||||
fe70f4b6568fbfec85e58d34f3cb8c3624daba65487d73ad24e981f2121cb27a packages/bus/src/runtime.mjs
|
||||
0accd20566096e06ee2f354cfee70da6e86b2f4091076eda6847253cdb9e036b packages/bus/src/usage.mjs
|
||||
7620eda0f16f4a126dad4e46887716afccd5638dc0028e8d989f0b5d3e4a6b00 packages/bus/tests/chief-of-staff.test.mjs
|
||||
d4a26f810298ee2a9222855318ec119353745eb8fbdbfe67e99ec591705b89c5 packages/bus/tests/credential-status.test.mjs
|
||||
dbbfa80b4082fd44e2f08539b5455653fac062321b8f2106f638e2d8161fa3eb packages/bus/tests/daily-quota-runtime.test.mjs
|
||||
2e649657ec73aeb2d8d7e3f1ff3015f8c71027e21815feca299330e311c81ff1 packages/bus/tests/end-launch.test.mjs
|
||||
a2e553025edbe6e930c9d03b48289c53b51b153ac9ae49bb21bf721b725f1913 packages/bus/tests/limits-runtime.test.mjs
|
||||
2942ee6e77ff99187bb31bbbb1d1df4f723a18fb799bf3b784dd18abe03b1ac3 packages/bus/tests/process.test.mjs
|
||||
6c5ad1a2b003abc469440e292a094b930246bfbb234c1076b969da5a53e05c7c packages/bus/tests/project-runtime.test.mjs
|
||||
409807e8ee248ce1a2ae4e887410871349b85d0d144d2c81acad01bb509b5a03 packages/bus/tests/routes-verb.test.mjs
|
||||
25bc6448389f095a22c3bf659b5accd01d78150c2e8af80c211951a680b8c061 packages/bus/tests/tasks.test.mjs
|
||||
404b7f33db494aa288a9d8e29358580a81e7db28819cf0f8e943e04f845ce0a7 packages/bus/tests/tester.test.mjs
|
||||
5abe93fca8442f06ff68760623e91262d863e1191fa35a3d0c87d7b0e6c0a4c6 packages/bus/tests/usage-runtime.test.mjs
|
||||
471a0b1bd84df8812281e47c915dfae86859fdf63fb257bd7aa4ceda7562362b packages/bus/tests/usage.test.mjs
|
||||
e3a447f3b3a57befe2b4d8ce34425c385c8f22cefea0bff19d16fa410088246e packages/business/README.md
|
||||
2862f7a409757ce930a34c6332e588da5f6ed5e4e25be90f89a07c163a9027ad packages/business/src/index.mjs
|
||||
c5f847de26356c97f09aee428e35f256885e8825860fa1b49448a1ab8bf2973b packages/business/src/instance-settings.mjs
|
||||
ee8d940631541dffb4f78af5c0ac95707d9c24d47be43b0d9e7573bab57a6015 packages/business/src/launch-settings.mjs
|
||||
7cac2ba23d333e447b5545c030c8d9686122f2fd31cc62c220bcf3703f8e9c7e packages/business/src/vars.mjs
|
||||
9be0521258a3fc7d044b494ec92e066ddeab9a3a33172d7eb54f24b0b97420a6 packages/business/tests/instance-settings.test.mjs
|
||||
77e560fd67f3390404bedb0fbd90b4e30f58255d1aa194598d141ac02e2aa4ad packages/business/tests/launch-settings.test.mjs
|
||||
140d7da07cfb0794be6438d8fb2fd504d3ec1b2fade244a396b802ff650ed009 packages/business/tests/vars.test.mjs
|
||||
fd0cc0a27035e10c253653ccc77997b4d0c3736e00fba6b9191a1e81c31043ba packages/cli/README.md
|
||||
6b2813071b857c58e7aae67a64b6f29d15fdca431e0dae8c0041988aeb699c23 packages/cli/src/launcher.mjs
|
||||
8a73002152c88c2850d466e5ee2c61fe6bcd44f03cea390f1ce8f282ca394b7a packages/cli/tests/helpers.mjs
|
||||
06c20167a843064e8df13d9ea3aaffd2a689a2326e313497be69677b4af64b7f packages/cli/tests/instances.test.mjs
|
||||
b132e102df8bfb4a6409ccd19971d06c201ddd290b6869c52bb3099c26101d82 packages/cli/tests/launch-limit-checks.test.mjs
|
||||
e7255685d7de5013f9d10450c863143b000616fd42d444eaa4b2c1fa17ece2a2 packages/cli/tests/launch-routes.test.mjs
|
||||
3e57437f3aa96ef491fe4f5146241bcacbc57cce5feadd544313c17eff055b2e packages/cli/tests/launcher.test.mjs
|
||||
aa1ef8517c3560d619b77d96d01bce00e50b8b6b7ac54013b632fcde874f49b2 packages/cli/tests/thread-launch.test.mjs
|
||||
c1ad84c544b6b1ea8a4897a77b3899c19c48705834ca19d963dadbe15fe73d50 packages/harness/README.md
|
||||
9ce5b05b20c9eb85c3c2acbabaee2f0157e8eeb13d91cb2c13c15834d6031585 packages/harness/src/bundle.mjs
|
||||
dd8df37ecee238f51e89e8e3c8ad0e71b1bacb046f8fafcd5b9c6a853e3a659f packages/harness/src/runner.mjs
|
||||
4ed9477ac21784d8e6fe1cd77c42fad90357854eb73bac2770454dcb7eda6aa1 packages/harness/tests/bundle.test.mjs
|
||||
d3769a4052eb66be0cc34eb4e012c7e6b0294b21cb732d0f79d077f3edf5e3af packages/harness/tests/claude-session.test.mjs
|
||||
f3a8ea7b81395b22e1a5612f46579e4248ef23c037483aa602a8a4adece617be packages/harness/tests/fixtures/fake-adapter.mjs
|
||||
7fb487227a90cf9bbb02b44084372cfece89bdeed33deed38afc441842ef2cb3 packages/harness/tests/helpers.mjs
|
||||
94dd7f4fef41870a769def394c7469d65f90a6e400534fefe62364b2a2d05619 packages/harness/tests/pi-session.test.mjs
|
||||
956f42cb77b571625adf6d0bdadf0ee5052b5c2f7e6160aa91a102d2d9bd81c3 packages/harness/tests/runner.test.mjs
|
||||
77fd7f2460f4f58abd78b4cb145391f5a08bef5ae20978ec77c55eceac97db2c packages/plugins/README.md
|
||||
fc65ec7e54f45617d7aab43f370bca5d6d88f9e27902188c3e8497255e7e541e packages/plugins/harness/claude-code/index.mjs
|
||||
1b7bef037f24b9eca2180630bb1d91dcfc47009eb5339b869ab469681b0d9a7b packages/plugins/harness/pi/index.mjs
|
||||
215bfcb058660239fc298b1e5459d5d44943543c2499af16f9726fd8cf9492da packages/plugins/src/kinds.mjs
|
||||
f9e8dbd79c4276a88759da112be0853d274918d8a5bce78ddd0a0b35c4cb47e7 packages/plugins/tests/harness.test.mjs
|
||||
ef3b670787bf8f75abf1a3113c46eadf17a3e2e984106926bfb18660efa247e1 packages/plugins/tests/kinds.test.mjs
|
||||
2f4f0e41baaf8ece16113674d7cca0d915e2fd34848845bd5776bc3d5f7f37b7 packages/plugins/tests/loader.test.mjs
|
||||
a862bbf8ace77b7fd143addb8c29bd89e08e1d22e2a24af6c54ca66dea186aa0 packages/seat/README.md
|
||||
8c1b67d791540c2bdca36c0f8048b00835b4333d9440f70a87ed71beb0d28299 packages/seat/src/sandbox.mjs
|
||||
2dd222d6519e365ae86e64768113b8cc2535a7a862368ad02496aa7dc6d0d050 packages/seat/src/session.mjs
|
||||
8c57fb0610f2f55f3b9491b9435b5ad26b45322a331f5e0bd5f1e928a024420d packages/seat/tests/sandbox.test.mjs
|
||||
62d5a75d4d84791bd99f1f38ba3fbd13a95b01c5f3e3917017288adc48e2dc0e packages/seat/tests/session.test.mjs
|
||||
fe1d1787df9d685871ec450125f889d9f80f32d726490f633065603515a394cf packages/webui/README.md
|
||||
0bdebc781dc865a1eb2bfcaa70dbbd12e649b2f6338b40a81b394f388d2fd63f packages/webui/src/cli.mjs
|
||||
acb46ebf39a88cead437c8c310d08a12c6f1f2fa1b0ab0af2de21fc308ffdd8b packages/webui/src/public/bus.js
|
||||
555ca97616b6616f2b3e3070304ad7c97d21b7f609b8807de82caaf5b0013c84 packages/webui/src/serve.mjs
|
||||
9a157959622653f358b248bf01eff7a80ca0af3d355fe40f7e2de5134ba73e17 packages/webui/tests/routes-browser.test.mjs
|
||||
642fd426432fbcd63fd5f6236a7715582f289f283257ca062ae1b4838aacf1b3 packages/webui/tests/routes.test.mjs
|
||||
624749a0345addc28abff6976ec0c89618d95d948a718eac8b4298af19f8b264 packages/webui/tests/settings-form.test.mjs
|
||||
ff0b4fce9af8ed8b779a6c4c96932d4039ba0f2f9a1b83d96acddc2d82cca92c packages/webui/tests/shell.test.mjs
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,99 @@
|
||||
diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js
|
||||
index 24d36d654..b0ffec0ce 100644
|
||||
--- a/packages/webui/src/public/bus.js
|
||||
+++ b/packages/webui/src/public/bus.js
|
||||
@@ -574,14 +574,14 @@
|
||||
const providerRows=[...providers].map(p=>field(['providers',p],p,limits.providers?.[p],usage.providers?.[p])).join('');
|
||||
const modelRows=[...pairs.values()].map(([p,m])=>field(['models',p,m],`${p} / ${m}`,limits.models?.[p]?.[m],usage.models?.[p]?.[m])).join('');
|
||||
const table=Object.entries(modelTable).map(([key,m])=>`<div class="filters"><strong>${txt(key)}</strong><label>Provider <input data-setting="${esc(inputKey(['table',key,'provider']))}" value="${esc(val(['table',key,'provider'],m.provider))}" maxlength="200"${disabled}></label><label>Model <input data-setting="${esc(inputKey(['table',key,'model']))}" value="${esc(val(['table',key,'model'],m.model))}" maxlength="200"${disabled}></label></div>`).join('');
|
||||
- const selections=Object.entries(v.instances||{}).map(([instance,entry])=>`<label>${txt(instance)} <select data-setting="${esc(inputKey(['instances',instance]))}"${disabled}>${Object.keys(modelTable).map(key=>`<option value="${esc(key)}"${val(['instances',instance],entry.modelKey)===key?' selected':''}>${txt(key)}</option>`).join('')}</select></label>`).join('');
|
||||
- return `<section class="panel" aria-labelledby="launch-settings-title"><h2 id="launch-settings-title">Agent limits</h2><p>Leave a limit blank for no limit. Zero stops new launches. Running sessions keep running when limits or models change.</p><form id="launch-settings-form"><fieldset><legend>All sessions</legend>${field(['total'],'Total',limits.total,usage.total)}</fieldset><fieldset><legend>By provider</legend><div class="filters">${providerRows}</div></fieldset><fieldset><legend>By provider and model</legend><div class="filters">${modelRows}</div></fieldset><fieldset><legend>Model table</legend>${table}</fieldset><fieldset><legend>Role models</legend><div class="filters">${selections}</div></fieldset><p class="small muted">${canSend?'Changes are checked and logged with your identity.':'Saving requires signed-in send mode.'}</p><button type="submit"${disabled}>${settingsSaving?'Saving…':'Save launch settings'}</button> <button type="button" id="launch-settings-reload">Read current settings and discard draft</button><p role="status" id="launch-settings-receipt">${txt(settingsReceipt)}</p></form></section>`;
|
||||
+ // Row 65 (mos-0840): each instance's model moved to the routing table below.
|
||||
+ return `<section class="panel" aria-labelledby="launch-settings-title"><h2 id="launch-settings-title">Agent limits</h2><p>Leave a limit blank for no limit. Zero stops new launches. Running sessions keep running when limits or models change.</p><form id="launch-settings-form"><fieldset><legend>All sessions</legend>${field(['total'],'Total',limits.total,usage.total)}</fieldset><fieldset><legend>By provider</legend><div class="filters">${providerRows}</div></fieldset><fieldset><legend>By provider and model</legend><div class="filters">${modelRows}</div></fieldset><fieldset><legend>Model table</legend>${table}</fieldset><p class="small muted">${canSend?'Changes are checked and logged with your identity.':'Saving requires signed-in send mode.'}</p><button type="submit"${disabled}>${settingsSaving?'Saving…':'Save launch settings'}</button> <button type="button" id="launch-settings-reload">Read current settings and discard draft</button><p role="status" id="launch-settings-receipt">${txt(settingsReceipt)}</p></form></section>`;
|
||||
}
|
||||
function settingDraft(form){const values={};for(const el of form.querySelectorAll('[data-setting]'))values[el.dataset.setting]=el.value;return values;}
|
||||
- function settingPayload(){const v=settingsSnapshot, limits={providers:Object.create(null),models:Object.create(null)},models=structuredClone(v.models),instances={};for(const [encoded,text]of Object.entries(settingsInputs)){const [kind,p,m]=JSON.parse(encoded);if(kind==='table'){models[p][m]=text;continue;}if(kind==='instances'){instances[p]=text;continue;}if(text==='')continue;if(!/^\d+$/.test(text)||!Number.isSafeInteger(Number(text)))throw Error('Limits need whole numbers of zero or more.');const n=Number(text);if(kind==='total')limits.total=n;else if(kind==='providers')limits.providers[p]=n;else {limits.models[p]??=Object.create(null);limits.models[p][m]=n;}}
|
||||
+ function settingPayload(){const v=settingsSnapshot, limits={providers:Object.create(null),models:Object.create(null)},models=structuredClone(v.models);for(const [encoded,text]of Object.entries(settingsInputs)){const [kind,p,m]=JSON.parse(encoded);if(kind==='table'){models[p][m]=text;continue;}if(text==='')continue;if(!/^\d+$/.test(text)||!Number.isSafeInteger(Number(text)))throw Error('Limits need whole numbers of zero or more.');const n=Number(text);if(kind==='total')limits.total=n;else if(kind==='providers')limits.providers[p]=n;else {limits.models[p]??=Object.create(null);limits.models[p][m]=n;}}
|
||||
// Row 125 (#1619): the form doesn't edit token limits; a save keeps them as read.
|
||||
if(v.limits?.tokens)limits.tokens=structuredClone(v.limits.tokens);
|
||||
- return {revision:v.revision,limits,models,instances};
|
||||
+ return {revision:v.revision,limits,models};
|
||||
}
|
||||
document.addEventListener('input',e=>{const form=e.target.closest?.('#launch-settings-form');if(form)settingsInputs=settingDraft(form);});
|
||||
document.addEventListener('change',e=>{const form=e.target.closest?.('#launch-settings-form');if(form)settingsInputs=settingDraft(form);});
|
||||
@@ -727,6 +727,59 @@
|
||||
else instanceReceipt=response.ok||result?.sent==='unknown'?unknownInstance:`${result?.message||'Refused.'} Draft kept.`;}}
|
||||
catch{instanceReceipt=unknownInstance;}finally{instanceBusy=false;if(!out())render(false);}
|
||||
});
|
||||
+ // Row 65 (mos-0840): the routing table. One row per instance: harness, model and thinking, and
|
||||
+ // the fallback's three. Each cell offers the switched-on harnesses, the model table's keys and the
|
||||
+ // levels that harness applies; a value the file holds that isn't offered stays, marked, so a save
|
||||
+ // sends it and the bus says why it refuses. A fallback is set by choosing all three or cleared by
|
||||
+ // choosing none for all three. Save sends the changed rows through routes.set. Drafts survive
|
||||
+ // polling and refusals; nothing retries.
|
||||
+ let routesView=null, routesDraft=null, routesBusy=false, routesReceipt='';
|
||||
+ const ROUTE_CELLS=[['harness','Harness'],['model','Model'],['thinking','Thinking'],['fallback.harness','Fallback harness'],['fallback.model','Fallback model'],['fallback.thinking','Fallback thinking']];
|
||||
+ const routeCells=r=>({harness:r.harness??'',model:r.model??'',thinking:r.thinking??'','fallback.harness':r.fallback?.harness??'','fallback.model':r.fallback?.model??'','fallback.thinking':r.fallback?.thinking??''});
|
||||
+ const sameCells=(a,b)=>ROUTE_CELLS.every(([k])=>a[k]===b[k]);
|
||||
+ function routesSection(view){
|
||||
+ const title='<h2 id="routes-title">Routing</h2>';
|
||||
+ if(!view)return `<section class="panel" aria-labelledby="routes-title">${title}<p class="small warn">The routes could not be read, so there is nothing to change. Read again to retry; they come from the bus host with your identity.</p></section>`;
|
||||
+ if(!routesDraft)routesView=view;
|
||||
+ const v=routesView, disabled=!canSend||out()||routesBusy?' disabled':'';
|
||||
+ const cells=name=>routesDraft?.[name]??routeCells(v.instances[name]);
|
||||
+ const select=(name,key,list,current,why)=>{const offered=current===''||list.includes(current)?list:[...list,current];
|
||||
+ return `<select data-route="${esc(inputKey([name,key]))}" aria-label="${esc(`${name} ${ROUTE_CELLS.find(([k])=>k===key)[1].toLowerCase()}`)}"${disabled}>${key.startsWith('fallback.')||current===''?`<option value=""${current===''?' selected':''}>${key.startsWith('fallback.')?'none':'unset'}</option>`:''}${offered.map(x=>`<option value="${esc(x)}"${x===current?' selected':''}>${txt(x)}${list.includes(x)?'':` (${why})`}</option>`).join('')}</select>`;};
|
||||
+ const levelsOf=h=>Array.isArray(v.levels[h])?v.levels[h]:[];
|
||||
+ const rows=Object.keys(v.instances).map(name=>{const c=cells(name);
|
||||
+ const harness=(key)=>select(name,key,v.harnesses,c[key],'not switched on');
|
||||
+ const model=(key)=>select(name,key,v.models,c[key],'not in the model table');
|
||||
+ const thinking=(key,h)=>select(name,key,levelsOf(c[h]),c[key],c[h]?`not applied by ${c[h]}`:'no harness');
|
||||
+ return `<tr><th scope="row">${txt(name)}</th><td>${harness('harness')}</td><td>${model('model')}</td><td>${thinking('thinking','harness')}</td><td>${harness('fallback.harness')}</td><td>${model('fallback.model')}</td><td>${thinking('fallback.thinking','fallback.harness')}</td></tr>`;}).join('');
|
||||
+ const off=v.harnesses.length?'':'<p class="small warn">No harness is switched on in the business file\'s plugins.harness, so every launch refuses harness-off and a save that changes a harness refuses too. <code>mosaic plugins set</code> switches one on.</p>';
|
||||
+ return `<section class="panel" aria-labelledby="routes-title">${title}<p>Each instance's harness, model and thinking, and the fallback a launch takes when the primary harness fails its preflight. A fallback sets all three or none. Running sessions keep their route; the next launch reads the change.</p>${off}<form id="routes-form"><div class="table-wrap" tabindex="0" role="region" aria-label="Routes, scroll for all columns"><table id="routes-table"><thead><tr><th scope="col">Instance</th>${ROUTE_CELLS.map(([,l])=>`<th scope="col">${l}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table></div><p class="small muted">${canSend?'Changes are checked and logged with your identity.':'Saving requires signed-in send mode.'}</p><button type="submit"${disabled}>${routesBusy?'Saving…':'Save routes'}</button> <button type="button" id="routes-reload">Read current routes and discard draft</button><p role="status" id="routes-receipt">${txt(routesReceipt)}</p></form></section>`;
|
||||
+ }
|
||||
+ function routesFields(form){const draft=Object.fromEntries(Object.keys(routesView.instances).map(name=>[name,routeCells(routesView.instances[name])]));for(const el of form.querySelectorAll('[data-route]')){const [name,key]=JSON.parse(el.dataset.route);if(draft[name])draft[name][key]=el.value;}return draft;}
|
||||
+ // The changed rows only. A fallback with all three cells empty is cleared; one with some set goes
|
||||
+ // as it is, and the bus refuses it as partial.
|
||||
+ function routesPayload(draft){
|
||||
+ const routes={};
|
||||
+ for(const [name,c]of Object.entries(draft)){if(sameCells(c,routeCells(routesView.instances[name])))continue;
|
||||
+ const set=['harness','model','thinking'].filter(k=>c[`fallback.${k}`]!=='');
|
||||
+ routes[name]={harness:c.harness,model:c.model,thinking:c.thinking,fallback:set.length===0?null:Object.fromEntries(set.map(k=>[k,c[`fallback.${k}`]]))};}
|
||||
+ if(!Object.keys(routes).length)throw Error('Nothing changed.');
|
||||
+ return {revision:routesView.revision,routes};
|
||||
+ }
|
||||
+ const unknownRoutes='Outcome unknown. Read the routes again before trying. Draft kept.';
|
||||
+ const routesAnswer=r=>/^[a-f0-9]{64}$/.test(r?.revision??'')&&Array.isArray(r.harnesses)&&Array.isArray(r.models)&&[r.levels,r.instances].every(o=>o&&typeof o==='object'&&!Array.isArray(o));
|
||||
+ for(const kind of ['input','change'])document.addEventListener(kind,e=>{const form=e.target?.closest?.('#routes-form');if(!form||!routesView)return;routesDraft=routesFields(form);
|
||||
+ // A harness change redraws the row so its thinking cell offers that harness's levels.
|
||||
+ if(kind==='change'&&/harness"\]$/.test(e.target.dataset?.route??''))render(false);});
|
||||
+ document.addEventListener('click',e=>{if(e.target.id!=='routes-reload')return;routesDraft=null;routesView=null;routesReceipt='';last.delete('routes');render(true);});
|
||||
+ document.addEventListener('submit',async e=>{
|
||||
+ if(e.target.id!=='routes-form')return;e.preventDefault();if(!canSend||out()||routesBusy||!routesView)return;
|
||||
+ routesDraft=routesFields(e.target);let payload;try{payload=routesPayload(routesDraft);}catch(err){routesReceipt=err.message;render(false);return;}
|
||||
+ routesBusy=true;routesReceipt='';render(false);
|
||||
+ try{const response=await fetch('/api/routes',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){routesReceipt='Sign in again. Draft kept.';signOut();}else{const result=await response.json();
|
||||
+ if(response.ok&&routesAnswer(result)){routesDraft=null;routesView=result;const prior=last.get('routes');if(prior)last.set('routes',{...prior,rows:[result]});routesReceipt=`Routes saved for ${Object.keys(payload.routes).join(', ')}. The next launch of each uses the new route.`;}
|
||||
+ else routesReceipt=response.ok||result?.sent==='unknown'?unknownRoutes:`${result?.message||'Refused.'} Draft kept.`;}}
|
||||
+ catch{routesReceipt=unknownRoutes;}finally{routesBusy=false;if(!out())render(false);}
|
||||
+ });
|
||||
async function settingsView(get) {
|
||||
const s = await get.api('settings'), n = s.notifier || {};
|
||||
let usage, names = null;
|
||||
@@ -734,12 +787,14 @@
|
||||
try { names = await get('names'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; /* the section says the name could not be read */ }
|
||||
let instances = null;
|
||||
try { [instances] = await get('instances'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; }
|
||||
+ let routes = null;
|
||||
+ try { [routes] = await get('routes'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; }
|
||||
let limitRows = [], limitsRefused = false;
|
||||
try { limitRows = await get('limits'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; limitsRefused = true; }
|
||||
const notify = typeof n.binding === 'string'
|
||||
? `<dl class="kv"><dt>Binding</dt><dd><code>${txt(n.binding)}</code></dd></dl><p class="small muted">The binding name only. Its channels, server and user ids stay in the binding file.</p>`
|
||||
: empty('No notifier binding to show.', `${n.refused ? `<code>${txt(n.refused)}</code>: ` : ''}${txt(n.message || 'the notifier settings could not be read')}`);
|
||||
- return `${head('Settings', `${readLine()} Agent limits and appearance.`)}${appearance()}${launchForm(s.launch)}${assistantSection(s.launch, names)}${instancesSection(instances)}${tokenLimits(limitRows, limitsRefused)}${tokenUsage(usage)}
|
||||
+ return `${head('Settings', `${readLine()} Agent limits and appearance.`)}${appearance()}${launchForm(s.launch)}${routesSection(routes)}${assistantSection(s.launch, names)}${instancesSection(instances)}${tokenLimits(limitRows, limitsRefused)}${tokenUsage(usage)}
|
||||
<section aria-labelledby="st-notify"><h2 id="st-notify">Notifications</h2>${notify}</section>
|
||||
<section aria-labelledby="st-about" class="panel"><h2 id="st-about">About</h2><dl class="kv"><dt>Address</dt><dd><code>${txt(s.address) || 'unknown'}</code></dd><dt>Release</dt><dd>${s.release ? `<code>${txt(s.release)}</code>` : '<span class="muted">unknown: no RELEASE file was read</span>'}</dd><dt>Business</dt><dd>${txt(s.business) || none}</dd><dt>Board</dt><dd><code>${txt(s.board) || 'unknown'}</code></dd></dl></section>`;
|
||||
}
|
||||
Reference in new issue
Block a user