darkwing: row 155 packet, rounds 1-3 and the follow-up brief draft (#1649)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
b2f57ca70d
commit
66f9e9d0c6
9 files changed
+5430
No files matched your search
@@ -0,0 +1,457 @@
|
||||
# Row 155 (#1649): the human and the chief of staff configure the assistant
|
||||
|
||||
Builder: Darkwing. Brief: `docs/plans/2026-10-16_q169-assistant-config.md`, section "The human
|
||||
and the chief of staff configure the assistant", blob b2aeba0364dd (rev 1717). Jason's ruling Q169.
|
||||
Base: refactor 7ce156259, origin/refactor after row 125 landed (rebased 2026-10-18; built on
|
||||
9098c33f7, then 99d2b49c9). Worktree: `~/darkwing-scratch/r155/wt-125`; `wt` (9098c33f7) and
|
||||
`wt2` (99d2b49c9) are kept unchanged. Reviewers: Dewey (security) and Filbert.
|
||||
|
||||
Status: code, tests and docs written, 30 modified files plus `packages/webui/tests/assistant.test.mjs`.
|
||||
Rebased twice: onto 99d2b49c9 (rows 149, 124, 129 and 164; see "Rebase onto 99d2b49c9"), then
|
||||
onto row 125's landing (see "Rebase onto row 125" and "Row 125 landed"). The run on 99d2b49c9
|
||||
had 27 of 28 files green; the red one, webui `shell.test.mjs`, was my miss from the first round
|
||||
and is fixed. The run of all 39 files on 7ce156259 is green (see "Row 125 landed"). The first
|
||||
round's runs on 9098c33f7 (sage-0720) are under "Single-file runs". The full gate has not run;
|
||||
it is Sage's. Candidate: `r3/build.patch` and `r3/candidate-manifest.sha256` on 0473574b0
|
||||
(see "Candidate round 3"); round 2 was `r2/` on 449cadd1f, round 1 `r1/` on 7ce156259.
|
||||
|
||||
## Rebase onto 99d2b49c9
|
||||
|
||||
Three commits since 9098c33f7 touch this row's files: 70504d23a (row 124, token usage),
|
||||
03bbd2c98 (row 149, roles and harness) and 850484a7d (row 129, Console chat). I saved the old
|
||||
worktree's diff against 9098c33f7 (`~/darkwing-scratch/r155/out/pre-rebase-9098c33f7.patch`),
|
||||
added a new worktree at 99d2b49c9 and applied the diff with `git apply --3way`. 26 files
|
||||
applied cleanly. Three webui files conflicted, and each resolved as a union of both sides:
|
||||
|
||||
- `src/cli.mjs`: row 129's `chat = createChat({ call })` sits beside this row's `nameWrite`
|
||||
and `assistantStop`, and `startServer` gets all three.
|
||||
- `src/serve.mjs`: `BUS_VERBS` ends `'messages', 'usage', 'names'`, and `startServer` takes
|
||||
`chat`, `nameWrite` and `assistantStop`.
|
||||
- `src/public/bus.js`: row 124's `tokenUsage` stays above this row's assistant block.
|
||||
`settingsView` reads `usage` first, keeping row 124's rethrow on 403 or signed-out, then
|
||||
`names`, and renders the launch form, the Assistant section, then Token usage.
|
||||
|
||||
`webui/tests/assistant.test.mjs` is untracked in the old worktree, so the diff-based patch
|
||||
did not carry it. I copied it over unchanged (sha256 4e921ce653cc…) and marked it
|
||||
intent-to-add. Every changed source passes `node --check`, `public/bus.js` parses, and
|
||||
`git diff --check` is clean. The rebased diff is `out/rebased-99d2b49c9.patch` in my scratch:
|
||||
30 files, +800 -104.
|
||||
|
||||
The rebased run (`~/darkwing-scratch/r155/run-rebased.sh`, each file singly) covers the
|
||||
15 files of the first round plus the tests rows 124, 129 and 149 added or changed beside the
|
||||
merged sources: webui `usage`, `usage-browser`, `chat`, `chat-browser`, `browser`, `shell`
|
||||
and `views`; bus `usage`, `usage-broker`, `usage-runtime` and `workflow-roles`; business
|
||||
`workflow-roles`; harness `raise-domain`. 28 files.
|
||||
|
||||
Rebased run (sage-0876), 2026-10-18T19:58:43Z to 19:59:52Z, behind `busy.sh`, TMPDIR in my
|
||||
scratch: 27 of 28 files green, 236 pass and 1 fail summed over the logs. Per-file totals are in
|
||||
`~/darkwing-scratch/r155/out/20261018T195844Z/summary.txt`. The sweep found 0 leftovers.
|
||||
|
||||
The failure is webui `shell.test.mjs`, the write-boundary test. It lists every POST, PUT, PATCH
|
||||
and DELETE `fetch(...)` in `public/bus.js` and counts `method:`. This row's `assistantPost`
|
||||
added `fetch(url, {method:'POST', ...})`, so the list had a fourth entry and the count was 4,
|
||||
not 3. The assertion is the same at 9098c33f7, and row 129 did not change it. The first round
|
||||
ran only the files I touched, so it never ran `shell.test.mjs`. This was my miss, not a rebase
|
||||
effect.
|
||||
|
||||
The fix is in `shell.test.mjs` only:
|
||||
|
||||
- The expected list gets the `fetch(url, ...)` entry second, in source order.
|
||||
- The `method:` count goes from 3 to 4.
|
||||
- A new assertion pins every `assistantPost(` first argument to `url` (the definition),
|
||||
`'/api/settings'`, `'/api/name'` and `'/api/assistant-stop'`. A fourth caller, or a computed
|
||||
route, fails it.
|
||||
|
||||
`public/bus.js` is unchanged by the fix; its diff against 99d2b49c9 still has sha256
|
||||
e37389fd…, the one going to Mos. The rerun of `shell.test.mjs`, with `settings-form` and
|
||||
`assistant` as controls, waits for a slot. The rebased diff with the fix is
|
||||
`out/rebased-99d2b49c9-r2.patch`: 31 files, +804 -106.
|
||||
|
||||
## Rebase onto row 125 (sage-0879)
|
||||
|
||||
Row 125 lands before this row and touches 11 of its files, so the run on 99d2b49c9 is not the
|
||||
candidate. I dry-ran the rebase onto row 125's candidate 5289ca1d2 in
|
||||
`~/darkwing-scratch/r155/wt-dry`. `git apply --3way` left 12 conflict blocks in 8 files.
|
||||
`~/darkwing-scratch/r155/rebase/resolve125.py` resolves each block by index and refuses on a
|
||||
count mismatch. Every resolution is a union:
|
||||
|
||||
- `docs/TOOLS.md`: row 125's token-limit paragraph, then this row's Assistant paragraph.
|
||||
- Organization keys. There is one list, row 125's, with `assistants` appended:
|
||||
`revision, limits, models, instances, reset, assistants`. It is the same in `SETTINGS_KEYS`
|
||||
(`launch-settings.mjs`), both `ORGANIZATION_KEYS` (`broker.mjs`, `harness/src/tools.mjs`;
|
||||
row 125's `tools.test.mjs` holds the three equal) and the Console's `writable` (`serve.mjs`).
|
||||
This row's `UPDATE_KEYS` is gone; `set` refuses a patch with no key other than `revision`
|
||||
by reading `SETTINGS_KEYS`.
|
||||
- `cli/src/limits.mjs`: row 125's `reset PATH...` verb and `formatTokens`, and this row's
|
||||
`PATCH keys:` list, now with `reset`. `cli/tests/limits.test.mjs` pins the new list.
|
||||
- `broker.mjs` `#claimEnd`: row 125's signature with `by = s.run ?? s.human` and both comments.
|
||||
This row's callers pass five arguments, so their `by` is unchanged.
|
||||
- `harness/src/tools.mjs`: `change_organization` describes both the assistant fields and the
|
||||
token-limit reset, and its schema has both `reset` and `assistants`.
|
||||
- `public/bus.js` `settingsView`: reads usage, names and token limits, and renders the launch
|
||||
form, Assistant, Token limits, then Token usage.
|
||||
- `serve.mjs`: `BUS_VERBS` ends `'usage', 'limits', 'names'`. The refusal text names "a reset
|
||||
or assistant settings".
|
||||
- `shell.test.mjs`: five writes in source order (settings, row 125's reset post, `fetch(url,
|
||||
...)`, project file, bus message), `method:` counted 5, and this row's call-site pin.
|
||||
|
||||
`node --check` passes on every changed source and `git diff --check` is clean. A static
|
||||
evaluation of the shell test's regexes over the merged `bus.js` matches the expected list.
|
||||
`assistant.test.mjs` came through with sha256 4e921ce653cc…. The dry diff is
|
||||
`out/dry-5289ca1d2.patch` (31 files, +801 -104).
|
||||
|
||||
When row 125 lands, I redo the rebase on the landed commit, compare it with the dry run, and
|
||||
run `~/darkwing-scratch/r155/run-125.sh` on Sage's slot. That is the 28 files above plus 11 row
|
||||
125 test files on the merged sources: bus `limit-checks`, `limit-stop`, `limits-runtime` and
|
||||
`process`; business `token-limit-settings`; cli `launch-limits`, `launch-limit-checks`,
|
||||
`limits-tokens` and `thread-launch`; webui `limits-route` and `limits-browser`.
|
||||
|
||||
Sage's ruling sage-0713: no schema step (no 3i; row 124 keeps 3g to 3h). Row 134 stays its own
|
||||
row, waits for row 155 to be done (rev 1722) and rebases on this landing. Until row 134, the
|
||||
revision the chief of staff sends comes from the human; the bus, business, harness and cli
|
||||
READMEs, `docs/TOOLS.md`, the tool description and `roles/chief-of-staff.md` say so. Row 156
|
||||
also touches `broker.mjs` and the role md files; whichever lands second rebases.
|
||||
|
||||
## Row 125 landed (sage-0882)
|
||||
|
||||
Row 125 landed as aa625565f, and origin/refactor is now 7ce156259. The two commits after
|
||||
row 125's packet (df4d5702f) touch only `docs/SESSIONS.md` and
|
||||
`docs/plans/2026-10-14_prd-1.2-build.md`, neither in this row, so I based on 7ce156259 rather
|
||||
than df4d5702f. aa625565f has the same tree as the dry run's base 5289ca1d2 (`git diff`
|
||||
empty).
|
||||
|
||||
In `~/darkwing-scratch/r155/wt-125`, `git apply --3way` of `out/rebased-99d2b49c9-r2.patch`
|
||||
left the same 8 files in conflict, and `resolve125.py` resolved the same 12 blocks with no
|
||||
count mismatch. `node --check` passes on every changed source and `git diff --check` is clean.
|
||||
`out/rebased-7ce156259.patch` is byte-identical to `out/dry-5289ca1d2.patch` (sha256
|
||||
4dda2fc2…, 31 files, +801 -104), index lines included. `assistant.test.mjs` came through with
|
||||
sha256 4e921ce653cc…, staged this time, not intent-to-add. The shell test's own regex over the
|
||||
merged `bus.js` gives 5 writes equal to its expected list, 5 `method:` and the four
|
||||
`assistantPost(` arguments. The `bus.js` diff for Mos is `out/busjs-7ce156259.diff`, sha256
|
||||
9f9ce31b…, +53 -2.
|
||||
|
||||
Run (sage-0890), 2026-10-18T20:30:29Z to 20:31:58Z, `~/darkwing-scratch/r155/run-125.sh` with
|
||||
`WT=~/darkwing-scratch/r155/wt-125`, behind `busy.sh`, TMPDIR in my scratch, each file singly:
|
||||
39 of 39 files green. The awk sum over the 39 logs is 300 pass and 0 fail, with 0 skipped and 0
|
||||
todo. `shell.test.mjs` passed 5 of 5. Per-file totals are in
|
||||
`~/darkwing-scratch/r155/out/20261018T203030Z/`. The sweep found 0 leftovers.
|
||||
|
||||
## Candidate
|
||||
|
||||
`r1/build.patch` is the staged diff of `wt-125` against 7ce156259, byte-identical to
|
||||
`out/rebased-7ce156259.patch`: sha256 4dda2fc2b7b2971b6daacaab57a027938bd074463ec25bfeb6314095d8503b52,
|
||||
31 files, +801 -104. `r1/candidate-manifest.sha256` lists the sha256 of each of the 31 files
|
||||
in `wt-125`; its own sha256 is 949873cfda99a2e3d418efee0066fa1438e1c12f69b9d2dfa9413ee089a79183.
|
||||
The worktree had no unstaged change when both were written.
|
||||
|
||||
`busjs-7ce156259.diff` in this packet is the `public/bus.js` diff against 7ce156259 for Mos's
|
||||
decision 123 go: sha256 9f9ce31bc14e6c0ed6d1d6c9f6cc282992d0631192be6416e0163866264029cd,
|
||||
+53 -2. It is a copy of `out/busjs-7ce156259.diff` in my scratch.
|
||||
|
||||
## Candidate round 2 (sage-0900)
|
||||
|
||||
Row 162 landed as 7e7f8e208 and changed two of this row's 31 paths, `docs/TOOLS.md` and
|
||||
`packages/cli/README.md`, so the r1 manifest no longer described the tree this row would land
|
||||
on. Round 2 is confirm-only and makes no source change.
|
||||
|
||||
I applied `r1/build.patch` unchanged with `git apply --index` to a fresh worktree
|
||||
(`~/darkwing-scratch/r155/wt-r2`) at 449cadd1f, refactor HEAD. It applied without conflict,
|
||||
`git diff --cached --check` was clean, and the staged names are the same 31 files (+801 -104)
|
||||
with no unstaged change. No run on this base: Sage's landing gate covers it, and
|
||||
`host.test.mjs` reads the cli README.
|
||||
|
||||
`r2/build.patch` against BASE `449cadd1f` (sha256
|
||||
`2a5a470d4c6fb53ca9c6ee8ffe4f5d8cb4c49b36b95f3f5ea1bc523224d94170`).
|
||||
`r2/candidate-manifest.sha256` (sha256
|
||||
`b1989f39012df6cbdd0e676cf19f75ccf88fcbf9d1c0a502eb71ab4247efad25`).
|
||||
|
||||
What moved from r1:
|
||||
|
||||
- In the manifest, only `docs/TOOLS.md` (now `e8fa08bf…`) and `packages/cli/README.md` (now
|
||||
`7943058d…`). The other 29 sha256s are r1's.
|
||||
- In `r2/build.patch`, only lines for those two files: their `index` lines and three hunk
|
||||
headers (TOOLS.md `@@ -610` is now `@@ -636`, `@@ -627,6 +630` is now `@@ -653,6 +656`; the
|
||||
cli README `@@ -789` is now `@@ -795`). Row 162 added text above these hunks, so they start
|
||||
lower. With `index` and `@@` lines removed, r1 and r2 compare byte-identical.
|
||||
- `public/bus.js`: its diff on 449cadd1f has the same sha256 as `busjs-7ce156259.diff`
|
||||
(`9f9ce31b…`, +53 -2), index line included, since bus.js is unchanged between the two bases.
|
||||
Mos's decision 123 go (mos-0816) was on those lines.
|
||||
|
||||
Round 1 verdicts on manifest 949873cf: Filbert approved (comment 29010, rev 1849), Dewey
|
||||
approved (comment 29012, rev 1852). Each gave non-blocking follow-ups (see
|
||||
`followup-brief.md`, which carries Mos's bus.js notes and Dewey's F1 on `serve.mjs` null
|
||||
bodies; `body()` already refuses those, see its item 3).
|
||||
|
||||
## Candidate round 3 (sage-0906)
|
||||
|
||||
Sage's landing gate on r2 (candidate ba32dcad2 on 0473574b0, every suite run through, 20:53:10Z
|
||||
to 20:59:57Z) had one failure out of 2370: `packages/bus/tests/channel-proof.test.mjs:18`,
|
||||
"channel conversion preserves the four proof/transport byte baselines". Row 164 (008746b6e)
|
||||
pins the sha256 of `packages/webui/src/bus.mjs` at :14, and this row's eight STATUS entries in
|
||||
that file change it to `d1f3a274…e241`. My single-file runs missed it: the test reads
|
||||
`bus.mjs` by path, not by import, and it isn't beside any file this row touches. That was my
|
||||
miss. Two other tests read a manifest path literally (`stack-release`, `stack-copy` and webui
|
||||
`service` read `packages/webui/src/cli.mjs`); they passed on Sage's gate.
|
||||
|
||||
Sage ruled to bump the pin: the `bus.mjs` diff is STATUS table entries only, and the freeze
|
||||
guards the channel conversion, not later status codes. Round 3 changes only
|
||||
`channel-proof.test.mjs:14`, the `bus.mjs` baseline, from `dbad731c…1f24` to
|
||||
`d1f3a274f7aa15eb7ecc2267c65bf4fe7e45c898f77c34235b4e9c29cd1be241`, with a trailing comment
|
||||
naming row 155. The other three baselines (`human.mjs`, `human-cli.mjs`, `transport.mjs`) and
|
||||
the import check are unchanged, and none of those three files is in this row's manifest.
|
||||
|
||||
I applied `r2/build.patch` unchanged with `git apply --index` to a fresh worktree
|
||||
(`~/darkwing-scratch/r155/wt-r3`) at 0473574b0, then made the one edit. Run on Sage's slot
|
||||
(sage-0906), output in `~/darkwing-scratch/r155/out/r3-20261018T210058Z/`, 21:00:58Z to
|
||||
21:01:13Z:
|
||||
|
||||
- `channel-proof.test.mjs` singly: 2 pass, 0 fail.
|
||||
- Mutant, the `'name-taken': 409, …` STATUS line removed from `bus.mjs`: 1 pass, 1 fail, and
|
||||
the failure names `packages/webui/src/bus.mjs`. Restored after, sha256 checked against
|
||||
`d1f3a274…`.
|
||||
- The whole `packages/bus` suite once (`node --test tests/*.test.mjs`): 399 pass, 0 fail, 0
|
||||
skipped, 0 todo.
|
||||
|
||||
Totals are an awk sum over the logs. The sweep found 0.
|
||||
|
||||
`r3/build.patch` against BASE `0473574b0` (sha256
|
||||
`9bee0e1b6546e70f06c1781beadc67082aa14990228f25571cdaf72e85f8c37e`), 32 files.
|
||||
`r3/candidate-manifest.sha256` (sha256
|
||||
`96cae28e095eff1632b7edcba195e1793bcabc3fb5aa93ea86d485c35e1f9016`), 32 paths.
|
||||
|
||||
What moved from r2: one new manifest line, `packages/bus/tests/channel-proof.test.mjs`
|
||||
(`3e6d794b…`), and its one-line hunk in the patch. The other 31 manifest lines and every r2
|
||||
patch line, index and `@@` lines included, are unchanged. The `public/bus.js` diff still has
|
||||
sha256 `9f9ce31b…`, so Mos's go (mos-0816) applies.
|
||||
|
||||
## No schema step
|
||||
|
||||
The row needs no migration, so there is no 3i.
|
||||
|
||||
- `thread_claims` already has `op` (claim, release, revoke), `holder_run`, `by`, `reason` and
|
||||
`decision` (`packages/bus/schema.sql`). A thread revoke is one more row there.
|
||||
- `role.claim` on the thread path already refuses `run-revoked` for a run with a revoke row, so a
|
||||
revoked assistant run can't claim its thread again.
|
||||
- Names use row 141's `instance_names`. The new config key lives in the organization file, which
|
||||
isn't the bus store.
|
||||
|
||||
The comment at `broker.mjs:2215` says a thread claim is never revoked and `thread_claims` has no
|
||||
reason. Both stop being true here. `#claimEnd` now writes `reason` for a thread claim too, and the
|
||||
comment says so.
|
||||
|
||||
## 1. What "configure" changes
|
||||
|
||||
A new organization key, `assistants`:
|
||||
|
||||
```json
|
||||
{ "revision": "<64 hex>", "assistants": { "<instance>": { "vars": {}, "tracker": {}, "definition": "assistant" } } }
|
||||
```
|
||||
|
||||
- Each field given replaces that field of the entry. `tracker: null` removes the tracker. A field
|
||||
left out stays as it is. An empty entry is refused. An instance the file doesn't have is
|
||||
`invalid-request`.
|
||||
- Today `tracker: null` changes nothing in practice: the assistant definition uses no vikunja
|
||||
credential, so `validateBusinessDocument` refuses any tracker on it and none exists to remove.
|
||||
The field is there for a future assistant definition that has one.
|
||||
- The target instance's current definition has to be the thread definition, or the write refuses
|
||||
`organization-not-assistant`. A given `definition` has to be one the broker treats as the
|
||||
assistant, or it refuses `organization-definition-refused`. Today that list is one name,
|
||||
`assistant`, so a definition change is accepted only as a no-op. It's there so the field exists
|
||||
when a second assistant definition does.
|
||||
- `holder` and `credentials` aren't accepted (strict keys, `invalid-request`).
|
||||
- The whole next document still goes through `validateBusinessDocument` and the unknown-model check.
|
||||
A tracker on a definition with no vikunja credential is refused there, as it is for a hand edit.
|
||||
|
||||
No writer this row adds can give an instance a definition in `END_PROTECTED`. `assistants` only
|
||||
targets instances that are already the assistant, and `instances` still sets only `vars.model`. On
|
||||
top of that, `set()` now compares every instance's definition before and after and refuses
|
||||
`organization-definition-refused` if any other one changed. That closes the dewey-0546 case
|
||||
(an unprotected instance becoming the assistant after an arbiter resolved a revoke of it) for every
|
||||
key the writer takes, not only the new one. It's defense in depth: no key today can change a
|
||||
definition except `assistants`, which only accepts the same one.
|
||||
|
||||
*For Dewey.* `vars` replaces the whole object, including the `limits.tools`, `limits.network` and
|
||||
`limits.authority` vars. Those only narrow: `resolve.mjs:64-68` filters the role file's tools and
|
||||
authority by them and takes the narrower network, so a chief that writes wider ones gets nothing
|
||||
the role file doesn't grant. A chief can remove a narrowing the human set, back up to the role
|
||||
file. `checkVars` still checks every var.
|
||||
|
||||
One source for the definitions. `THREAD_DEFINITION` and `END_PROTECTED` move from `broker.mjs` into
|
||||
`packages/business/src/vocabulary.mjs`, which the broker already imports for `ID_PATTERN`. The
|
||||
broker and `launch-settings.mjs` read the same constants, so no equality test is needed. A test
|
||||
holds `END_PROTECTED` including `THREAD_DEFINITION`, so the assistant can't lose its protection by
|
||||
an edit to one list.
|
||||
|
||||
Files: `vocabulary.mjs`, `launch-settings.mjs` (`set` and `read`), `broker.mjs`
|
||||
(`ORGANIZATION_KEYS`, the two constants), `packages/harness/src/tools.mjs` (its
|
||||
`ORGANIZATION_KEYS` copy, which a test holds equal).
|
||||
|
||||
`read` adds `assistants`: each assistant instance's `definition`, `vars` and `tracker` as the file
|
||||
has them, so the human and the Console see what they're replacing.
|
||||
|
||||
## 2. Who configures
|
||||
|
||||
- **Human, CLI.** `mosaic limits set --file PATCH` already sends the whole patch to `limits.set`,
|
||||
so a patch with `assistants` works through it. `mosaic limits` prints the revision. The usage
|
||||
line, the cli README and `docs/TOOLS.md` name the new key. I'm not adding a second command for
|
||||
the same write.
|
||||
- **Human, Console.** The Settings view gets an Assistant section per assistant instance: the
|
||||
definition, the vars and the tracker as JSON fields, with the same Save, revision and receipt
|
||||
handling as the limits form. `settingsSend`'s writable list adds `assistants`.
|
||||
- **Chief of staff.** `change_organization` takes `assistants` as well (schema and description).
|
||||
It runs `organization.set`, which checks `organization.change` before the writer and consumes it
|
||||
after. Every other role is refused there, as today.
|
||||
- **Revision.** Both carry the file's revision and a stale one refuses `revision-conflict` in
|
||||
`organizationWriter.write`.
|
||||
- **Log.** `organization.mjs` already writes the actor, `before`, `after` and `changes` to
|
||||
`.history/<id>/changes.jsonl`. The actor is the human id, or `chief-of-staff.<instance>`.
|
||||
|
||||
**Gap: the chief can't read the revision.** No verb gives an agent the organization revision. Row
|
||||
134 ("Organization revision read for a role holding organization.change", queued) owns that read.
|
||||
Row 155 doesn't build it. Until row 134 lands, the chief's `change_organization` call works only
|
||||
with a revision it was given (by the human, in a message), and the tests pass the revision in
|
||||
directly. I've told Sage.
|
||||
|
||||
## 3. The seat name
|
||||
|
||||
- **Chief of staff.** A new typed tool, `set_name` (verb `name.set`, action `instance.name`), with
|
||||
`instance` and `name`. Row 141 added the grant and the broker check; this adds the tool. With it,
|
||||
`untypedActions(chief)` and the bundle's untyped list become `[]`, so `tools.test.mjs:43` and
|
||||
`bundle.test.mjs:94` are re-pinned.
|
||||
- **Human, Console.** A POST route, `/api/name`, with `{instance, name}` and the settings boundary
|
||||
(signed-in header user, exact Origin, same-origin Fetch Metadata, JSON, strict keys, 4 KiB). It
|
||||
calls `name.set` through `human-cli`, so the broker's `#human` check applies: a Console started
|
||||
inside an agent run gets `human-required`. The Assistant section shows the current name, read
|
||||
through a new `names` bus read. `names` is in `BUS_VERBS`, and the webui `cli.mjs` adds it beside
|
||||
the shared views (`bus = { ...views({ call }), names: () => call('names') }`); `bus/src/views.mjs`
|
||||
is unchanged, because the CLI doesn't need it. The route is `/api/name`; a second one,
|
||||
`/api/assistant-stop`, is in section 4.
|
||||
- **Human, CLI.** `mosaic name` already exists (row 141).
|
||||
- Every other role is refused by `name.set`'s within-role check.
|
||||
|
||||
## 4. Removing the assistant: ending its runs
|
||||
|
||||
**role.revoke and role.end of a threaded instance.** Today the end path reads `role_claims` through
|
||||
`#holder`, and the assistant claims in `thread_claims`, so the answer is `not-held`. For a threaded
|
||||
target the end path now collects every thread of that instance whose latest claim is `claim`, and
|
||||
writes a revoke for each. None held is still `not-held`. A role target keeps its one-claim path. The
|
||||
result adds `threads: N` for a threaded target.
|
||||
|
||||
- A chief `role.end` with a reason passes `#protectedEnd` (row 139) and ends them all.
|
||||
- A `role.revoke` with a decision the human resolved through the CLI passes `#protectedEnd` and ends
|
||||
them all. An arbiter can't resolve that decision at all: `#hardWait` makes a revoke of an
|
||||
`END_PROTECTED` target a hard wait, which goes to the human whoever raised it.
|
||||
- The chief's own run's end stays refused (`a.role === s.role`).
|
||||
|
||||
**The null and undefined holder run.** The raise records `holderRun` as
|
||||
`#holder(...)?.holder_run ?? null`, which is null for a threaded target. The check compared it with
|
||||
`#holder(...)?.holder_run`, undefined, and answered `decision-mismatch`. Both sides now use `?? null`.
|
||||
|
||||
*For Dewey.* For a role target the decision binds to the holder incarnation at the raise. For a
|
||||
threaded target there's no single incarnation, so it binds to nothing beyond the target, the
|
||||
action, the raiser and the class. It's still single-use (`#spent`) and still human-resolved through
|
||||
the CLI. If you want it bound tighter, the raise could record the set of held thread runs and the
|
||||
check could require the same set. I think that refuses a legitimate revoke whenever a thread idles
|
||||
out between raise and use, which is the common case.
|
||||
|
||||
**What the run does.** The runner polls every 2 s. Its next bus call gets `not-holder`, which is in
|
||||
`SESSION_GONE`, and it exits 22. A turn already running finishes first, the same as `role.end` of a
|
||||
role today. The launcher starts a new run only for a thread with pending messages or an open, so the
|
||||
assistant comes back only when the human writes to it again. Removing the instance from the
|
||||
organization file is out of scope.
|
||||
|
||||
**The human, Console.** A new human-only verb, `assistant.stop {instance}`.
|
||||
|
||||
- It checks `#human(s)` and that the instance is threaded.
|
||||
- It revokes the current claim of every thread of that instance the human owns (REQ-USER-2: a user
|
||||
reaches only their own threads), with `by` set to the human and reason `stopped by the human`.
|
||||
None held is `not-held`. A known instance that isn't threaded is a new code, `not-assistant`
|
||||
(webui status 400); an unknown one is `unknown-role`.
|
||||
- It returns the stopped runs.
|
||||
|
||||
The Console route is POST `/api/assistant-stop` with `{instance}` and the same boundary as `/api/name`.
|
||||
The button sits in the Assistant section. I chose per-instance over per-run because the Console
|
||||
lists no threads or thread runs (`agents` reads `role_claims` only), and stopping the assistant is
|
||||
what Q169's "remove" means. `mosaic stop <run>` still stops one run from the CLI.
|
||||
|
||||
The human can't use `role.revoke` itself: `#consumeAuthority` starts with `#agent`, which refuses a
|
||||
human.
|
||||
|
||||
## 5. Text
|
||||
|
||||
- `packages/harness/src/bundle.mjs:112` and the `end_run` description (`tools.mjs:166`) say the
|
||||
chief can't end the assistant's run. That's false after this row. Row 149's brief leaves the
|
||||
`end_run` text in `bundle.mjs` to row 155.
|
||||
- `roles/chief-of-staff.md:30-35`: the chief ends the assistant with `end_run`, configures it with
|
||||
`change_organization` (`assistants`, with a revision from the human until row 134), and names
|
||||
instances with `set_name`. Raising `policy.change` for those goes.
|
||||
|
||||
## What else changed
|
||||
|
||||
- `packages/cli/src/limits.mjs`: the usage line names `assistants`; `limits.test.mjs` pins it.
|
||||
- `packages/webui/src/bus.mjs`: statuses for the codes the new routes and `assistants` can return
|
||||
(`organization-not-assistant`, `organization-definition-refused`, `unknown-model`,
|
||||
`name-is-instance`, `name-is-role` and `not-assistant` 400; `name-taken` and `not-held` 409).
|
||||
- `roles/chief-of-staff.md`: the contract text names no person, because
|
||||
`business/tests/chief-of-staff.test.mjs` refuses `Jason` in it.
|
||||
- Re-pinned tests: `chief-of-staff.test.mjs:174`, `threads.test.mjs:396`, the bus and business
|
||||
`hard-waits.test.mjs`, `tools.test.mjs`, `bundle.test.mjs`, `runner.test.mjs` (its fixture's
|
||||
chief gets `instance.name`), `project-runtime.test.mjs` (the broker's import line) and webui
|
||||
`shell.test.mjs` (the write-boundary list, after the rebase; see "Rebase onto 99d2b49c9").
|
||||
|
||||
## Tests (brief item 5)
|
||||
|
||||
| Brief item | Where |
|
||||
|---|---|
|
||||
| Human CLI and Console changes of vars, tracker and definition succeed and log the human | business `launch-settings.test.mjs`, cli `limits.test.mjs`, webui `settings-write.test.mjs` |
|
||||
| Chief's change succeeds and logs the chief | bus `chief-of-staff.test.mjs` |
|
||||
| Another role's change is refused | bus `chief-of-staff.test.mjs` |
|
||||
| Definition change without protection refused; another instance given an END_PROTECTED definition refused | business `launch-settings.test.mjs` |
|
||||
| Chief-raised revoke of the assistant is a hard wait to the human, no arbiter resolves it | bus `hard-waits.test.mjs` |
|
||||
| Stale revision refused | business and bus |
|
||||
| Human's and chief's name changes succeed, another role's refused | bus `chief-of-staff.test.mjs` and row 141's `names.test.mjs`, harness `tools.test.mjs` (`set_name`), webui `assistant.test.mjs` |
|
||||
| Chief's end stops every assistant thread run; its own end stays refused | bus `threads.test.mjs` |
|
||||
| Human-resolved revoke stops them; arbiter-resolved stays refused | bus `threads.test.mjs` |
|
||||
| Console stop route ends an assistant run | bus `threads.test.mjs` (verb), webui `assistant.test.mjs` (route) |
|
||||
|
||||
Plus the re-pins above, a `set_name` tool test, and a browser test for the Assistant section in
|
||||
`settings-form.test.mjs`.
|
||||
|
||||
### Single-file runs (sage-0720)
|
||||
|
||||
`~/darkwing-scratch/r155/run-touched.sh`: one `node --test` per file from its package directory,
|
||||
behind `busy.sh`, TMPDIR `~/darkwing-scratch/r155/tmp`, logs teed. 15 files: the 13 this row
|
||||
touches (12 modified, `assistant.test.mjs` new) plus business `chief-of-staff.test.mjs` and bus
|
||||
`names.test.mjs`. No Docker, no full suites, no mutants.
|
||||
|
||||
- Run 1, 2026-10-18T15:34:50Z to 15:35:18Z: 13 green, 2 red.
|
||||
- bus `threads.test.mjs`, 14 of 15. My test bound an inside-agent human session to show it can't
|
||||
stop runs. `bindHuman` refuses that at the bind with human-required (pinned since
|
||||
`broker.test.mjs:132`), so the session never reaches the verb. The test now asserts the bind
|
||||
refusal and checks the verb with pm, chief and a. No broker change.
|
||||
- webui `settings-form.test.mjs`, the Q169 test. `render` always reads `/api/settings` again,
|
||||
so after a save the page took its revision from the fixture server, which only ever returned
|
||||
revision a. The next post carried a where the test expected b. The page is right: a real server
|
||||
returns the saved revision. The test's fetch mock now serves the saved state on reads after a
|
||||
save. No `bus.js` change.
|
||||
- Run 2, 15:38:33Z to 15:39:01Z: all 15 green, 167 pass, 0 fail. Logs:
|
||||
`~/darkwing-scratch/r155/out/20261018T153833Z/`. Sweep found 0 leftovers.
|
||||
|
||||
That run covers the fixtures' `project.create` and `thread.create` calls, the chief's
|
||||
`decision.raise` of a revoke with no grant, the decision-consumed check order and the browser
|
||||
test's timing, which were unproven before it.
|
||||
|
||||
## Overlaps
|
||||
|
||||
- Row 149 (Dewey, in review) changes `roles/chief-of-staff.md` Authority text and the `raise_decision`
|
||||
tool in `tools.mjs`. I'll rebase onto its landing before the in-review move and run every touched
|
||||
test file singly.
|
||||
- Row 156 (Sage, queued after 149) touches `broker.mjs` and the role md files. Whichever lands second
|
||||
rebases.
|
||||
- Row 124 (Rocko) does the Settings readout and schema 3h. The Settings view is shared; I'll check
|
||||
its diff at rebase.
|
||||
|
||||
## Gate
|
||||
|
||||
Dewey and Filbert approve on #1649 naming the candidate manifest. Mos's decision 123 go on the
|
||||
`public/` diff. Bus, business, harness, cli and webui suites and every `scripts/test-*.sh` green.
|
||||
@@ -0,0 +1,74 @@
|
||||
diff --git a/packages/webui/src/public/bus.js b/packages/webui/src/public/bus.js
|
||||
index 64a7d6674..1e0be849d 100644
|
||||
--- a/packages/webui/src/public/bus.js
|
||||
+++ b/packages/webui/src/public/bus.js
|
||||
@@ -621,16 +621,67 @@
|
||||
}
|
||||
return `<section class="panel" aria-labelledby="token-usage-title"><h2 id="token-usage-title">Token usage</h2><p>Best reading of session-editable transcripts, not proof. Unknown turns may have used more tokens. Account is unassigned until account tracking is configured.</p><div class="table-wrap"><table id="token-usage-table"><thead><tr><th scope="col">Subject</th><th scope="col">Input</th><th scope="col">Output</th><th scope="col">Cache write</th><th scope="col">Cache read</th><th scope="col">Unknown turns</th></tr></thead><tbody>${rows.join('')}</tbody></table></div></section>`;
|
||||
}
|
||||
+ // Row 155 (#1649, Q169): each assistant instance's settings, its seat name and a stop. A save
|
||||
+ // sends only the fields that changed, with the read's revision. Drafts survive polling and
|
||||
+ // refusals as in the limits form, and nothing retries.
|
||||
+ let assistantSnapshot=null, assistantDrafts=Object.create(null), nameDrafts=Object.create(null), assistantBusy=false, assistantReceipts=Object.create(null);
|
||||
+ const asShown=(entry,field)=>field==='definition'?entry.definition??'':entry[field]==null?'':JSON.stringify(entry[field],null,2);
|
||||
+ function assistantSection(value,names){
|
||||
+ if(!value||value.refused||!value.assistants||!Object.keys(value.assistants).length)return '';
|
||||
+ if(!Object.keys(assistantDrafts).length)assistantSnapshot=value;
|
||||
+ const v=assistantSnapshot, disabled=!canSend||out()||assistantBusy?' disabled':'';
|
||||
+ const named=names?Object.fromEntries(names.map(r=>[r.instance,r.name])):null;
|
||||
+ const forms=Object.entries(v.assistants).map(([instance,entry])=>{
|
||||
+ const d=assistantDrafts[instance]||{}, val=f=>Object.hasOwn(d,f)?d[f]:asShown(entry,f), k=esc(instance);
|
||||
+ const name=Object.hasOwn(nameDrafts,instance)?nameDrafts[instance]:named?.[instance]??'';
|
||||
+ return `<form class="assistant-form" data-instance="${k}"><fieldset><legend>${txt(instance)}</legend><div class="filters"><label>Seat name <input data-assistant-field="name" value="${esc(name)}" maxlength="64"${disabled}></label> <button type="button" data-assistant-name="${k}"${disabled}>Save name</button>${named?'':' <span class="small muted">The current name could not be read.</span>'}</div><label>Definition <input data-assistant-field="definition" value="${esc(val('definition'))}" maxlength="64"${disabled}></label><label>Vars (JSON) <textarea data-assistant-field="vars" rows="4"${disabled}>${esc(val('vars'))}</textarea></label><label>Tracker (JSON, empty removes it) <textarea data-assistant-field="tracker" rows="4"${disabled}>${esc(val('tracker'))}</textarea></label><button type="submit"${disabled}>Save assistant settings</button> <button type="button" data-assistant-stop="${k}"${disabled}>Stop its runs</button><p role="status" data-assistant-receipt="${k}">${txt(assistantReceipts[instance]||'')}</p></fieldset></form>`;
|
||||
+ }).join('');
|
||||
+ return `<section class="panel" aria-labelledby="assistant-title"><h2 id="assistant-title">Assistant</h2><p>The definition stays an assistant one. Holder and credentials stay hand edits. Stop ends the runs of your threads; the assistant starts again when you next write to it.</p>${forms}<p class="small muted">${canSend?'Changes are checked and logged with your identity.':'Changes require signed-in send mode.'}</p></section>`;
|
||||
+ }
|
||||
+ function assistantPayload(instance,form){
|
||||
+ const entry=assistantSnapshot.assistants[instance], patch={};
|
||||
+ for(const f of ['definition','vars','tracker']){
|
||||
+ const text=form.querySelector(`[data-assistant-field="${f}"]`).value, what=f==='vars'?'Vars':'The tracker';
|
||||
+ if(text===asShown(entry,f))continue;
|
||||
+ if(f==='definition'){patch.definition=text.trim();continue;}
|
||||
+ if(f==='tracker'&&text.trim()===''){patch.tracker=null;continue;}
|
||||
+ let parsed;try{parsed=JSON.parse(text);}catch{throw Error(`${what} must be JSON.`);}
|
||||
+ if(!parsed||typeof parsed!=='object'||Array.isArray(parsed))throw Error(`${what} must be a JSON object.`);
|
||||
+ patch[f]=parsed;
|
||||
+ }
|
||||
+ if(!Object.keys(patch).length)throw Error('Nothing changed.');
|
||||
+ return {revision:assistantSnapshot.revision,assistants:{[instance]:patch}};
|
||||
+ }
|
||||
+ const unknownAssistant='Outcome unknown. Read current settings before trying again. Draft kept.';
|
||||
+ async function assistantPost(url,payload,instance,ok){
|
||||
+ assistantBusy=true;assistantReceipts[instance]='';render(false);
|
||||
+ try{const response=await fetch(url,{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(payload),cache:'no-store',redirect:'manual'});if(response.type==='opaqueredirect'){assistantReceipts[instance]='Sign in again. Draft kept.';signOut();}else{const result=await response.json();assistantReceipts[instance]=response.ok?ok(result):result?.sent==='unknown'?unknownAssistant:`${result?.message||'Refused.'} Draft kept.`;}}
|
||||
+ catch{assistantReceipts[instance]=unknownAssistant;}finally{assistantBusy=false;if(!out())render(false);}
|
||||
+ }
|
||||
+ const assistantForm=el=>el?.closest?.('.assistant-form');
|
||||
+ for(const kind of ['input','change'])document.addEventListener(kind,e=>{const form=assistantForm(e.target);if(!form)return;const instance=form.dataset.instance,field=e.target.dataset.assistantField;if(field==='name')nameDrafts[instance]=e.target.value;else if(field)assistantDrafts[instance]={...Object.fromEntries(['definition','vars','tracker'].map(f=>[f,form.querySelector(`[data-assistant-field="${f}"]`).value]))};});
|
||||
+ document.addEventListener('submit',async e=>{
|
||||
+ const form=assistantForm(e.target);if(!form||e.target!==form)return;e.preventDefault();if(!canSend||out()||assistantBusy)return;const instance=form.dataset.instance;let payload;
|
||||
+ try{payload=assistantPayload(instance,form);}catch(err){assistantReceipts[instance]=err.message;render(false);return;}
|
||||
+ await assistantPost('/api/settings',payload,instance,result=>{if(!/^[a-f0-9]{64}$/.test(result?.revision??''))return unknownAssistant;delete assistantDrafts[instance];assistantSnapshot=result;const prior=last.get('api:settings');if(prior)last.set('api:settings',{...prior,launch:result});return 'Assistant settings saved.';});
|
||||
+ });
|
||||
+ document.addEventListener('click',async e=>{
|
||||
+ const form=assistantForm(e.target);if(!form||!canSend||out()||assistantBusy)return;const instance=form.dataset.instance;
|
||||
+ if(e.target.dataset.assistantName!==undefined){const name=form.querySelector('[data-assistant-field="name"]').value.trim();
|
||||
+ await assistantPost('/api/name',{instance,name},instance,result=>{if(typeof result?.name!=='string')return unknownAssistant;delete nameDrafts[instance];const prior=last.get('names');if(prior)last.set('names',{...prior,rows:[...prior.rows.filter(r=>r.instance!==result.instance),{instance:result.instance,name:result.name,at:result.at}]});return `Seat name set to ${result.name}.`;});}
|
||||
+ else if(e.target.dataset.assistantStop!==undefined)await assistantPost('/api/assistant-stop',{instance},instance,result=>Array.isArray(result?.stopped)?`Stopped ${plural(result.stopped.length,'run')}.`:unknownAssistant);
|
||||
+ });
|
||||
async function settingsView(get) {
|
||||
const s = await get.api('settings'), n = s.notifier || {};
|
||||
- let usage;
|
||||
+ let usage, names = null;
|
||||
try { [usage] = await get('usage'); } catch (err) { if (err.status === 403 || err.code === 'signed-out') throw err; }
|
||||
+ try { names = await get('names'); } catch { /* the section says the name could not be read */ }
|
||||
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)}${tokenLimits(limitRows, limitsRefused)}${tokenUsage(usage)}
|
||||
+ return `${head('Settings', `${readLine()} Agent limits and appearance.`)}${appearance()}${launchForm(s.launch)}${assistantSection(s.launch, names)}${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>`;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Follow-up row brief: Settings page reads and snapshots after Q169 (row 155 review notes)
|
||||
|
||||
Draft by Darkwing for Sage (sage-0899), from Mos's two `public/bus.js` notes on row 155, which
|
||||
Mos (mos-0816) and Filbert (#1649 comment 29010) ruled non-blocking, plus a third instance I
|
||||
found while checking them, and Dewey's F1 (#1649 comment 29012, sage-0903; withdrawn in 29014). Line numbers are `packages/webui/src/public/bus.js` in row 155's
|
||||
candidate (manifest 949873cf…, base 7ce156259). This row starts after row 155 lands and rebases
|
||||
on that landing.
|
||||
|
||||
Suggested gate: Filbert approves the candidate manifest; Mos's decision 123 go on the
|
||||
`public/` diff; webui suite and every `scripts/test-*.sh` green on Sage's gate. Suggested owner:
|
||||
Darkwing, reviewer Filbert, gate owner Sage.
|
||||
|
||||
Neither item loses data today. (a) shows an empty name field where it should show the sign-in
|
||||
state. (b) makes the next save fail with `revision-conflict`, which keeps the draft and tells
|
||||
the human to reread. The fix makes the page match the other reads and stop sending a revision
|
||||
it already knows is old.
|
||||
|
||||
## Items
|
||||
|
||||
1. (a) The names read rethrows like the others. `settingsView` (:674) reads `usage` and
|
||||
`limits` with a catch that rethrows a 403 or `signed-out` and keeps any other failure local.
|
||||
The `names` read at :678 catches everything, so a signed-out or forbidden answer becomes "the
|
||||
name could not be read". Give it the same catch: rethrow 403 and `signed-out`, and keep the
|
||||
empty field and its text for any other failure. Test (webui `settings-form` or
|
||||
`assistant`): a fixture whose `names` read answers 403 gets the same page as a 403 on
|
||||
`usage`, and a fixture whose `names` read fails another way still renders the Assistant
|
||||
section with "could not be read".
|
||||
|
||||
2. (b) One settings snapshot. The page holds the launch settings twice: `settingsSnapshot`
|
||||
(:559) for the launch form and the token-limit reset, and `assistantSnapshot` (:627) for the
|
||||
Assistant section. Each save refreshes only its own:
|
||||
- the launch form save (:587) sets `settingsSnapshot`, not `assistantSnapshot`;
|
||||
- the assistant save (:666) sets `assistantSnapshot`, not `settingsSnapshot`;
|
||||
- row 125's reset (:609) sets `settingsSnapshot` when no launch draft is open, never
|
||||
`assistantSnapshot`.
|
||||
|
||||
So a save in one form while the other has an open draft leaves the other with the old
|
||||
revision, and its next save is refused. The render paths re-snapshot only when their own
|
||||
form has no draft (:563, :631), which is what keeps the stale copy alive.
|
||||
|
||||
Fix: one snapshot for both forms. Every successful save or reset sets it from the answer,
|
||||
and drafts stay per form (`settingsInputs`, `assistantDrafts`, `nameDrafts`), so an open
|
||||
draft keeps its typed values over the new snapshot. The launch form payload (:577) already
|
||||
builds from the snapshot plus its inputs; the assistant payload (:642-653) does the same with
|
||||
only the changed fields. The reload button (:584) clears the one snapshot. Test, one per
|
||||
path, in webui `settings-form` (the launch and assistant saves) and `limits-browser` (the
|
||||
reset):
|
||||
- with a limits draft open, save the assistant, then submit the limits draft: its posted
|
||||
revision is the one the assistant save returned, and the typed limit is still in the field;
|
||||
- with an assistant draft open, save the launch form, then submit the assistant draft: its
|
||||
posted revision is the one the launch save returned;
|
||||
- with an assistant draft open, reset a token limit, then submit the assistant draft: its
|
||||
posted revision is the one the reset returned.
|
||||
|
||||
Each test fails on row 155's candidate, where the second post carries the old revision.
|
||||
|
||||
3. (F1) Null bodies on the Console write routes. Dewey's F1: `Object.keys(value)` throws on a
|
||||
JSON `null` in `seatSend` (`packages/webui/src/serve.mjs:241`, the new `/api/name` and
|
||||
`/api/assistant-stop` routes), `settingsSend` (:203) and `projectFileSend` (:276), and an
|
||||
async handler throw with no `unhandledRejection` handler ends the Console.
|
||||
|
||||
I checked it against the candidate, and that throw can't happen there. Each handler reads
|
||||
its body through `body()` (:35, from ea00ec66d) inside a `try`, and `body()` parses the JSON
|
||||
and throws `body must be a JSON object` for `null`, an array or any non-object (:45-46). The
|
||||
`catch` answers 400 `invalid-request` before the `Object.keys` line runs. Two of the four
|
||||
already have passing tests for it: `settings-write.test.mjs:17` posts `null` to
|
||||
`/api/settings` and expects 400, and `project-file.test.mjs:76` ("a body that is JSON but not
|
||||
an object is refused 400, not thrown on") posts `null`, `[]`, `5`, `"text"` and `true` and
|
||||
expects 400 with "JSON object". The two `seatSend` routes have no such test.
|
||||
|
||||
Dewey withdrew F1 in #1649 comment 29014 (rev 1856) and agreed the item is tests only, with
|
||||
no local check beyond `body()`. Test, one per `seatSend` route in `assistant.test.mjs`:
|
||||
posting `null` to `/api/name` and to `/api/assistant-stop` gets 400 `invalid-request`, no
|
||||
write call is made, and a following valid request still gets an answer. The settings and
|
||||
project file routes keep their existing tests. Both new tests pass on row 155's candidate;
|
||||
they pin `body()`'s check on the two routes that lack one.
|
||||
|
||||
The shell write-boundary test (`shell.test.mjs`) pins every POST `fetch` and every
|
||||
`assistantPost(` route in `bus.js`. None of the three adds or changes a write, so its list and
|
||||
count stay as row 155 leaves them; if the fix touches a `fetch(` line, that test changes with it.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Server-side settings handling, the routes, and any change to what a save sends beyond the
|
||||
revision.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
685f1ffd74fb5dc09eb30f2f4e8f9d8ef524b4c11956520322df59b4cf71971f docs/TOOLS.md
|
||||
00e3097588604ee3dac5159bcbb430aba5ed2380ae3eea94eb2ad4c58ac47d58 packages/bus/README.md
|
||||
b4def5241ba1dc2655bfcd014d6053797c89c79f23a536d7591573e4977df6d9 packages/bus/src/broker.mjs
|
||||
a898b99ef887934655ed89dd7f39f1844a390eb71c5f06f77e9cdc121ee1be69 packages/bus/tests/chief-of-staff.test.mjs
|
||||
9c9ec893dfb4b4afe6e3131584a4a59b57f8704020efd51347b3d82fde46e5a3 packages/bus/tests/hard-waits.test.mjs
|
||||
536194fabad0608f75f7ee594df0612513766631511197a149507df02cd615d9 packages/bus/tests/project-runtime.test.mjs
|
||||
3d3006280a0cdf5a3df41e6ffcf02a716fcd9a1f98547597cacea4c4bc9c5133 packages/bus/tests/threads.test.mjs
|
||||
9cb665ed7f9b2acc9ea9738e78b63bf27d69ffb731d14b363c3cd92b23a1441a packages/business/README.md
|
||||
f3085de0a6f7ced053e99bc860d05714189ded1700abab962bfc7200c41fc401 packages/business/src/launch-settings.mjs
|
||||
bfc317915a776cbac15c190740b74b808bb03a9d8d88b9f11e054ae4fcb0b164 packages/business/src/vocabulary.mjs
|
||||
174f1745fdf0120d79b09e7834e07021a41528443e1e30098fa947681d971510 packages/business/tests/hard-waits.test.mjs
|
||||
303bba96b97ba287bd9c0164451d0007ce8682bb74235a7d613292cf3db276cc packages/business/tests/launch-settings.test.mjs
|
||||
353ef3302041f9d042bc75c85bf50e17a1239d14741c085fd8b11e3dc6c60dcb packages/cli/README.md
|
||||
696cbfcd6646441d82cdcb46ebe797f890cef6059cf885e8a8a418489ba05398 packages/cli/src/limits.mjs
|
||||
1d3d4b0c2f93930011648c2ec8bf748d68681f374ade3c3f1789531f53393e8e packages/cli/tests/limits.test.mjs
|
||||
4c80fa8aea72854c2349cc54767b2b530bf0063c7cdb9869f2401266d41688c2 packages/harness/README.md
|
||||
899664fc1b9980c6d2718e18fd3829bf8aa1ef49454779d2c99b24684f834132 packages/harness/src/bundle.mjs
|
||||
73d1db38e677be659a5455bb25f246aca2ab4187967b81543e63c994846e7186 packages/harness/src/tools.mjs
|
||||
847768146342afa5fed673d30caa7e6f421c895167e7f654941968bf0a8a8dd9 packages/harness/tests/bundle.test.mjs
|
||||
16b08c3bc7cc1fd9992d02134227ff51fff79a9977989c50ddd37afe00fec900 packages/harness/tests/runner.test.mjs
|
||||
1a823bb1581fc76e214e1dd772ed1ce411e0741f0391b25b5b21141d7029e9bd packages/harness/tests/tools.test.mjs
|
||||
aa60183c3c897299ac40e36413b3c02ecaf660f10bc6121757278382c28ecb55 packages/webui/README.md
|
||||
d1f3a274f7aa15eb7ecc2267c65bf4fe7e45c898f77c34235b4e9c29cd1be241 packages/webui/src/bus.mjs
|
||||
da66a46b10d15fb7f128c4f60951e9a45ef3c82c858b6fb30b365149b0c022fa packages/webui/src/cli.mjs
|
||||
51bf72804c85110bdeff2a94cf0178307dbc6ca874bfe4c4f9a53150ead9f093 packages/webui/src/public/bus.js
|
||||
eedebc55889b43a14339dd299c1ea8e32e46996018a008ef0e01560140fee8c0 packages/webui/src/serve.mjs
|
||||
4e921ce653cc387191778b9c6b214ba81ef79a64e37ca655371ad95f067fcfab packages/webui/tests/assistant.test.mjs
|
||||
2809d81c3dac4495bc58f3fc3b905bfb5ab6908fb92b465fbccd13f276ef64a5 packages/webui/tests/settings-form.test.mjs
|
||||
52413621f80273cf88742448035ae5e9602b780c7a356008f791f170f8d55891 packages/webui/tests/settings-write.test.mjs
|
||||
698ef27cebd404b5390bd5298ab271245c386eb3c8de11810eb3e9dc9336cd14 packages/webui/tests/shell.test.mjs
|
||||
823d540726937ed40b09e11cf92a4cd0c04689506c820dcccfbc18e69ec36e70 roles/chief-of-staff.md
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,31 @@
|
||||
e8fa08bf668d83aa9d30c795b409aeaea292422ef53c8e96c9aba7504c69de86 docs/TOOLS.md
|
||||
00e3097588604ee3dac5159bcbb430aba5ed2380ae3eea94eb2ad4c58ac47d58 packages/bus/README.md
|
||||
b4def5241ba1dc2655bfcd014d6053797c89c79f23a536d7591573e4977df6d9 packages/bus/src/broker.mjs
|
||||
a898b99ef887934655ed89dd7f39f1844a390eb71c5f06f77e9cdc121ee1be69 packages/bus/tests/chief-of-staff.test.mjs
|
||||
9c9ec893dfb4b4afe6e3131584a4a59b57f8704020efd51347b3d82fde46e5a3 packages/bus/tests/hard-waits.test.mjs
|
||||
536194fabad0608f75f7ee594df0612513766631511197a149507df02cd615d9 packages/bus/tests/project-runtime.test.mjs
|
||||
3d3006280a0cdf5a3df41e6ffcf02a716fcd9a1f98547597cacea4c4bc9c5133 packages/bus/tests/threads.test.mjs
|
||||
9cb665ed7f9b2acc9ea9738e78b63bf27d69ffb731d14b363c3cd92b23a1441a packages/business/README.md
|
||||
f3085de0a6f7ced053e99bc860d05714189ded1700abab962bfc7200c41fc401 packages/business/src/launch-settings.mjs
|
||||
bfc317915a776cbac15c190740b74b808bb03a9d8d88b9f11e054ae4fcb0b164 packages/business/src/vocabulary.mjs
|
||||
174f1745fdf0120d79b09e7834e07021a41528443e1e30098fa947681d971510 packages/business/tests/hard-waits.test.mjs
|
||||
303bba96b97ba287bd9c0164451d0007ce8682bb74235a7d613292cf3db276cc packages/business/tests/launch-settings.test.mjs
|
||||
7943058d0f97cde05f921fe8fe5e2bfac2198ea7c56476ba52e36ad735d3fd0f packages/cli/README.md
|
||||
696cbfcd6646441d82cdcb46ebe797f890cef6059cf885e8a8a418489ba05398 packages/cli/src/limits.mjs
|
||||
1d3d4b0c2f93930011648c2ec8bf748d68681f374ade3c3f1789531f53393e8e packages/cli/tests/limits.test.mjs
|
||||
4c80fa8aea72854c2349cc54767b2b530bf0063c7cdb9869f2401266d41688c2 packages/harness/README.md
|
||||
899664fc1b9980c6d2718e18fd3829bf8aa1ef49454779d2c99b24684f834132 packages/harness/src/bundle.mjs
|
||||
73d1db38e677be659a5455bb25f246aca2ab4187967b81543e63c994846e7186 packages/harness/src/tools.mjs
|
||||
847768146342afa5fed673d30caa7e6f421c895167e7f654941968bf0a8a8dd9 packages/harness/tests/bundle.test.mjs
|
||||
16b08c3bc7cc1fd9992d02134227ff51fff79a9977989c50ddd37afe00fec900 packages/harness/tests/runner.test.mjs
|
||||
1a823bb1581fc76e214e1dd772ed1ce411e0741f0391b25b5b21141d7029e9bd packages/harness/tests/tools.test.mjs
|
||||
aa60183c3c897299ac40e36413b3c02ecaf660f10bc6121757278382c28ecb55 packages/webui/README.md
|
||||
d1f3a274f7aa15eb7ecc2267c65bf4fe7e45c898f77c34235b4e9c29cd1be241 packages/webui/src/bus.mjs
|
||||
da66a46b10d15fb7f128c4f60951e9a45ef3c82c858b6fb30b365149b0c022fa packages/webui/src/cli.mjs
|
||||
51bf72804c85110bdeff2a94cf0178307dbc6ca874bfe4c4f9a53150ead9f093 packages/webui/src/public/bus.js
|
||||
eedebc55889b43a14339dd299c1ea8e32e46996018a008ef0e01560140fee8c0 packages/webui/src/serve.mjs
|
||||
4e921ce653cc387191778b9c6b214ba81ef79a64e37ca655371ad95f067fcfab packages/webui/tests/assistant.test.mjs
|
||||
2809d81c3dac4495bc58f3fc3b905bfb5ab6908fb92b465fbccd13f276ef64a5 packages/webui/tests/settings-form.test.mjs
|
||||
52413621f80273cf88742448035ae5e9602b780c7a356008f791f170f8d55891 packages/webui/tests/settings-write.test.mjs
|
||||
698ef27cebd404b5390bd5298ab271245c386eb3c8de11810eb3e9dc9336cd14 packages/webui/tests/shell.test.mjs
|
||||
823d540726937ed40b09e11cf92a4cd0c04689506c820dcccfbc18e69ec36e70 roles/chief-of-staff.md
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,32 @@
|
||||
e8fa08bf668d83aa9d30c795b409aeaea292422ef53c8e96c9aba7504c69de86 docs/TOOLS.md
|
||||
00e3097588604ee3dac5159bcbb430aba5ed2380ae3eea94eb2ad4c58ac47d58 packages/bus/README.md
|
||||
b4def5241ba1dc2655bfcd014d6053797c89c79f23a536d7591573e4977df6d9 packages/bus/src/broker.mjs
|
||||
3e6d794b6831fc162886079ec28d275ad69d4aaf84bad80a88e28afb9aee2619 packages/bus/tests/channel-proof.test.mjs
|
||||
a898b99ef887934655ed89dd7f39f1844a390eb71c5f06f77e9cdc121ee1be69 packages/bus/tests/chief-of-staff.test.mjs
|
||||
9c9ec893dfb4b4afe6e3131584a4a59b57f8704020efd51347b3d82fde46e5a3 packages/bus/tests/hard-waits.test.mjs
|
||||
536194fabad0608f75f7ee594df0612513766631511197a149507df02cd615d9 packages/bus/tests/project-runtime.test.mjs
|
||||
3d3006280a0cdf5a3df41e6ffcf02a716fcd9a1f98547597cacea4c4bc9c5133 packages/bus/tests/threads.test.mjs
|
||||
9cb665ed7f9b2acc9ea9738e78b63bf27d69ffb731d14b363c3cd92b23a1441a packages/business/README.md
|
||||
f3085de0a6f7ced053e99bc860d05714189ded1700abab962bfc7200c41fc401 packages/business/src/launch-settings.mjs
|
||||
bfc317915a776cbac15c190740b74b808bb03a9d8d88b9f11e054ae4fcb0b164 packages/business/src/vocabulary.mjs
|
||||
174f1745fdf0120d79b09e7834e07021a41528443e1e30098fa947681d971510 packages/business/tests/hard-waits.test.mjs
|
||||
303bba96b97ba287bd9c0164451d0007ce8682bb74235a7d613292cf3db276cc packages/business/tests/launch-settings.test.mjs
|
||||
7943058d0f97cde05f921fe8fe5e2bfac2198ea7c56476ba52e36ad735d3fd0f packages/cli/README.md
|
||||
696cbfcd6646441d82cdcb46ebe797f890cef6059cf885e8a8a418489ba05398 packages/cli/src/limits.mjs
|
||||
1d3d4b0c2f93930011648c2ec8bf748d68681f374ade3c3f1789531f53393e8e packages/cli/tests/limits.test.mjs
|
||||
4c80fa8aea72854c2349cc54767b2b530bf0063c7cdb9869f2401266d41688c2 packages/harness/README.md
|
||||
899664fc1b9980c6d2718e18fd3829bf8aa1ef49454779d2c99b24684f834132 packages/harness/src/bundle.mjs
|
||||
73d1db38e677be659a5455bb25f246aca2ab4187967b81543e63c994846e7186 packages/harness/src/tools.mjs
|
||||
847768146342afa5fed673d30caa7e6f421c895167e7f654941968bf0a8a8dd9 packages/harness/tests/bundle.test.mjs
|
||||
16b08c3bc7cc1fd9992d02134227ff51fff79a9977989c50ddd37afe00fec900 packages/harness/tests/runner.test.mjs
|
||||
1a823bb1581fc76e214e1dd772ed1ce411e0741f0391b25b5b21141d7029e9bd packages/harness/tests/tools.test.mjs
|
||||
aa60183c3c897299ac40e36413b3c02ecaf660f10bc6121757278382c28ecb55 packages/webui/README.md
|
||||
d1f3a274f7aa15eb7ecc2267c65bf4fe7e45c898f77c34235b4e9c29cd1be241 packages/webui/src/bus.mjs
|
||||
da66a46b10d15fb7f128c4f60951e9a45ef3c82c858b6fb30b365149b0c022fa packages/webui/src/cli.mjs
|
||||
51bf72804c85110bdeff2a94cf0178307dbc6ca874bfe4c4f9a53150ead9f093 packages/webui/src/public/bus.js
|
||||
eedebc55889b43a14339dd299c1ea8e32e46996018a008ef0e01560140fee8c0 packages/webui/src/serve.mjs
|
||||
4e921ce653cc387191778b9c6b214ba81ef79a64e37ca655371ad95f067fcfab packages/webui/tests/assistant.test.mjs
|
||||
2809d81c3dac4495bc58f3fc3b905bfb5ab6908fb92b465fbccd13f276ef64a5 packages/webui/tests/settings-form.test.mjs
|
||||
52413621f80273cf88742448035ae5e9602b780c7a356008f791f170f8d55891 packages/webui/tests/settings-write.test.mjs
|
||||
698ef27cebd404b5390bd5298ab271245c386eb3c8de11810eb3e9dc9336cd14 packages/webui/tests/shell.test.mjs
|
||||
823d540726937ed40b09e11cf92a4cd0c04689506c820dcccfbc18e69ec36e70 roles/chief-of-staff.md
|
||||
Reference in new issue
Block a user