# #1782: the owner sees a thread agent's claims (design r1) Morgana, 2026-10-22, for Duckworth. Brief: docs/plans/2026-10-22_owner-thread-claims.md (Sage, for mos-1262). The brief wasn't committed and had no queue row when this was written, so the folder is named for the brief. It moves to queue- once the row exists. Base: refactor c27207f6e (205 landed at ce6a96ad6, then 268). No code yet. ## What the code does now - A thread run's `role.claim` writes `thread_claims` only and returns (broker.mjs:2682 at ce6a96ad6). - `agents` (broker.mjs:3409) reads `role_claims` only: - human and reader sessions get `#agentState(c)` per held role; - role and tester sessions get the raw rows. - A held thread claim is the thread's latest `thread_claims` row with op `claim`. `#threadHolders` (:483) and `#threadRows` (:624) already compute it that way, with `c.seq = max(seq)` per thread. - `threads.name` is the name a user wrote. `threads.instance` is the role instance from the business file, which the operator writes, not the user. ## The problem with the brief's shape The brief adds thread rows to the human and reader `agents` result, after the role rows. Every consumer of that result assumes one row per role: | Consumer | What it reads | What a thread row would do | |---|---|---| | Console Agents view, webui bus.js:474 | `c.role` in a "Held roles" table, `claims.length` as its count | blank role cell, wrong count | | Console board held table, bus.js:447 and :462 `held-count` | same | same | | Console composer, bus.js:298 | `c.role`, filtered with `typeof r === 'string'` | dropped safely | | CLI TUI agents view, screens.mjs:280 and :284, app.mjs:288 and :433 | `c.role` as title and column | blank title | | CLI `mosaic agents`, format.mjs:54 | `c.role` | `undefined` in the line (brief item 4 covers this one) | So the shape as briefed also needs the Console (webui) and the TUI changed in this row, or the live Agents view breaks on the next re-pin. The brief's manifest names only format.mjs. ## Proposed shape: a separate owner verb The brief allows this ("for example a separate owner verb, it goes in BUILD.md to Duckworth before code"). **D1. New verb `thread.claims`**, human and reader sessions only. Every other session refuses with `human-required`, the code `decisions` uses at :3426. No arguments (`keys(a, [], [])`). The result, in thread creation order: ```js [{ thread, instance, holder_run, harness, at }] ``` - One row per thread whose latest `thread_claims` op is `claim`. - `at` is that claim row's time. - It doesn't return `name`, message content or `address`. `address` matches the brief's four fields; it can be added if Duckworth wants it next to the role rows. - `instance` is beyond the brief's four. It names which assistant holds the thread, and the operator writes it, not the user. SQL, the `#threadHolders` join without the instance and owner filter: ```sql SELECT t.id AS thread, t.instance, c.holder_run, c.harness, c.at FROM threads t JOIN thread_claims c ON c.seq=(SELECT max(seq) FROM thread_claims WHERE thread=t.id) WHERE t.business=? AND c.op='claim' ORDER BY t.seq ``` **D2. `agents` doesn't change** for any session. That is stronger than brief item 2 (role sessions only), and it means the Console, TUI and composer need no change in this row. **D3. Reader allow-list.** `thread.claims` goes in the reader list at :2370. The processing gate (:402) already exempts human and reader sessions, so a halt doesn't hide claims. No TESTER_VERBS or USER_VERBS entry is added (see D6). **D4. CLI `mosaic agents`** (cli.mjs:314) calls `agents` and then `thread.claims`. `formatAgents(list, threads = [])` prints the role lines unchanged, then for each held thread claim: ``` thread since ``` Each line starts with `thread `, so a reader can tell the two kinds apart (brief item 1). Role output with no thread claims is byte-equal to today, including "agents: no role is claimed". When there are no roles but there are thread claims, the empty-roles line is still printed, then the thread lines. **D5. The governance pin.** message-filter.test.mjs, the "claims" item: `threadsState` becomes `call(owner, 'thread.claims')`, which must include `{ thread: U, holder_run: 'assistant-run' }`. The comment that names this row goes away (brief item 5). **D6. What this row leaves out**, so as not to widen beyond owner and reader sessions (mos-1202): - tester sessions; - FED R3 user principals, who own threads under R3, so their own claims are an R6 question; - a Console section; - the TUI. ## Questions for Duckworth and Sage - **Q1.** Do you accept D1 over the briefed mixed list? If you'd rather have the mixed list, the manifest grows to webui bus.js, screens.mjs and app.mjs, plus their tests (bus-browser, webui bus.test and the TUI tests). - **Q2.** Should the Console show thread claims before a user thread goes live? PRD :1083-1084 promises visibility to the owner, and a Console user won't run `mosaic agents`. My recommendation: a follow-up row bound like this one (it lands before any user thread goes live), so this row stays small for 10-27. The alternative is a "Thread claims" section in the Console Agents view in this row. ## Tests (under D1) All new tests go in packages/bus/tests/agents.test.mjs unless named otherwise. Each one runs on a store with one held role claim and one thread run. - **T1.** Human `thread.claims` lists the held thread claim (thread, instance, holder_run, harness, at). Reader gets the same result. - **T2.** After release, the thread is gone. After a re-claim followed by a revoke, it's gone again. The thread has an earlier `claim` row, so a first-op reading would still list it. - **T3.** A role session's `thread.claims` refuses `human-required`. - **T4.** `agents` is deepEqual to a copy taken before the thread run claimed, for the role, human and reader sessions on the same store. - **T5.** `JSON.stringify` of the `thread.claims` result has neither the thread's name nor the body of a message sent in that thread. - **T6.** packages/cli/tests/format.test.mjs: a mixed list renders the role lines exactly as today, then `thread …` lines. The empty-roles case with threads is covered too. - **T7.** packages/bus/tests/message-filter.test.mjs: the moved pin (D5). ## Mutants (each killed by a named test) | Mutant | Change | Killed by | |---|---|---| | M1 | `thread.claims` returns `[]` | T1, T7 | | M2 | the held check uses `min(seq)` (first op) | T2 | | M3 | the human/reader check is dropped, so a role session gets thread rows | T3 | | M4 | `agents` for human appends the thread rows | T4 | | M5 | the SELECT adds `t.name` | T5 | | M6 | formatAgents drops the thread lines | T6 | ## Manifest (expected) - packages/bus/src/broker.mjs - packages/bus/README.md (a paragraph for `thread.claims`, next to `agents`) - packages/bus/tests/agents.test.mjs - packages/bus/tests/message-filter.test.mjs - packages/cli/src/cli.mjs - packages/cli/src/format.mjs - packages/cli/tests/format.test.mjs - this BUILD.md Verb-list pins checked at c27207f6e: - tester-writes.test.mjs:19 pins `TESTER_VERBS` exactly. D6 adds nothing to it, so the pin holds. - No test pins the reader list at :2370. - `thread.messages` appears in tester-threads, threads and webui chat as calls, not as a verb set. Before the in-review move, I'll rerun the same grep for `thread.claims` and for any new verb-set pin. Gate (brief): Duckworth approves the manifest. On Sage's gate, the touched test files run whole, the bus and cli suites are green, every scripts/test-*.sh is green, and M1-M6 are killed. ## r2 (duckworth-0118: D1 approved with C1-C3) Q1: Duckworth accepts D1. Q2: Duckworth is fine with a follow-up row under the same bind; Sage rules on it. Duckworth's note is agents/duckworth/work/row1782/DESIGN-r1.md on duckworth/reviews 13fb9abc5. **C1. `--tester` makes one call.** callFor binds the tester when `--tester` is given (cli.mjs:192). D6 keeps `thread.claims` off TESTER_VERBS, so a second call there would refuse tester-refused and exit 3. tester.test.mjs:128 pins exactly one spawn, `agents`. So cli.mjs calls `thread.claims` only when `--tester` is absent. A tester's `mosaic agents` is unchanged. - T8 (packages/cli/tests/format.test.mjs or the CLI test that drives main() with a stand-in transport; the exact file is picked at build): `agents --tester` makes one call, `agents`; human `agents` makes two, `agents` then `thread.claims`. - Gate adds tester.test.mjs and other-tree.test.mjs, each run whole. - M7: thread.claims is called on the tester path too. T8 and tester.test.mjs:128 kill it. **C2. `--json` shape.** `mosaic agents --json` prints the `agents` array unchanged, as `inbox --json` prints decisions only (cli.mjs:308). Only the text output adds the `thread …` lines. The machine read of thread claims is the bus verb, and the bus README paragraph documents `thread.claims` as such. A grep at c27207f6e found no consumer of `mosaic agents --json` in packages/*/tests or scripts. T6 pins both: the text output is mixed, and the `--json` output deepEquals the `agents` result alone. **C3. Where the pin moves.** Under D1 the governance pin moves to the owner's `thread.claims` call, not to `agents`. That is brief item 5 read under D1: the brief named `agents` because it put thread rows there. Notes taken: - N1: T4's before/after equality covers the tester session too. - N2: T3 adds a tester: `thread.claims` refuses tester-refused. - N3: FED R3 user principals must not get other users' thread claims. This row gives them none (no USER_VERBS entry). The rule carries with 205 N1/N3 to 205b/R3. - N4: the :403 processing-gate list is in the verb-set grep. Human and reader sessions return before that list (:402), so `thread.claims` needs no entry there, and no test pins the list (grep at c27207f6e). The :2370 reader list is the one list that gains the verb. Mutants are now M1-M7. Building starts once Sage creates the row. ## r3: build (sage-2486, on 23c3d6218) Changes from the design, each a placement choice: - **Bus tests file.** T1-T5 go in a new file, packages/bus/tests/thread-claims.test.mjs, not agents.test.mjs. agents.test.mjs's businesses have no `assistant` instance, so a thread run would need a new fixture there. The new file copies message-filter.test.mjs's setup (chief plus an `assistant` thread instance, human, reader and tester sessions). Its threads are owned by the business human (`jason`), so the human's `assistant.stop` reaches the run for T2's revoke. - **T3** covers the chief's role session and the thread run itself (both `human-required`) and the tester (`tester-refused`, N2). - **T4** covers human, reader, tester and chief sessions (N1). - **T5** writes the thread's message to the store, as message-filter.test.mjs's put() does. The run's own message.send would also pass through the messaging graph, which T5 doesn't test. - **T6/T8 split.** format.test.mjs holds T6's text cases: mixed, empty roles with threads, and no threads argument. The `--json` pin needs main(), so it sits with T8 in tester.test.mjs, whose setup has both stand-in transports. That test also pins the call order: tester `["agents"]`; human `["agents", "thread.claims"]`; human `--json` adds only `agents`. Its JSON deepEquals the broker's own `agents` result. - **T7.** The message-filter.test.mjs pin now reads `holder_run` from the owner's `thread.claims`. The old pin read `holder` from `threadsState`. Manifest (actual): - packages/bus/src/broker.mjs - packages/bus/README.md (a "Thread claims" paragraph at the end of "Agent state") - packages/bus/tests/thread-claims.test.mjs (new) - packages/bus/tests/message-filter.test.mjs - packages/cli/src/cli.mjs - packages/cli/src/format.mjs - packages/cli/tests/format.test.mjs - packages/cli/tests/tester.test.mjs - agents/morgana/work/queue-277/BUILD.md Mutants: M1 (T1, T7), M2 (T2), M3 (T3), M4 (T1, T4), M5 (T5), M6 (T6), M7 (T8). Gate: the touched test files run whole, plus other-tree.test.mjs. Then the bus and cli suites and every scripts/test-*.sh. Nothing runs before Sage's CLEAR. Early look (sage-2504, 05:27Z), on c050bb90b: thread-claims.test.mjs failed 5/5. The test's `call` helper took any string as a session name, but a run's capability is a string too, so the run's calls went out unauthenticated. T1 also compared null-prototype store rows strictly. Both are test bugs, and the fix is r2 of the build. On r2, thread-claims 5/5, cli tester 9/9, other-tree 4/4 pass, 0 skipped. ## r3: Duckworth notes N1/N2 (duckworth-0119, ruled sage-2505), for the post-194 rebase round - **N1.** A human `mosaic agents` no longer fails as a whole when `thread.claims` is refused. cli.mjs prints the role lines, then `thread claims: refused `, and exits 0. The choice of exit 0 follows D1: `agents` stays as it was, and it exited 0 before this row. Only an error with a bus code is caught (the same `^[a-z-]{1,64}$` shape `markRead` checks); any other error still throws. Pinned by a new test in cli tester.test.mjs: a stand-in transport refuses thread.claims with `processing-halted`, the role row prints, then the refusal line. This matches row 278 item 4 for the Console. M8 (the catch drops the refusal line, or rethrows) is killed by that test. - **N2.** packages/cli/README.md: the usage line gains `[--tester]`, and a bullet describes the `thread` lines, the refusal line, `--json` and `--tester`. host.test.mjs:855 reads this README for a unit file name, which is still there. host.test.mjs joins the gate's whole-file runs. Manifest adds packages/cli/README.md (10 paths). Nothing here has run yet. It runs whole after the rebase onto 194, on Sage's CLEAR. ## r4: rebase onto 0165be266 and Duckworth's delta notes (sage-2587, duckworth-0123) - **Rebase.** Commits 1-6 are cherry-picked onto refactor 0165be266, which has 194, 236 and 274 landed, on morgana/row277-0165. The range-diff is `=` for all six and the patch-ids are unchanged. Four files auto-merged around upstream lines: bus README, broker.mjs, cli README and cli.mjs. - **N1 (duckworth-0123).** The N1 catch's rethrow branch had no test, so a mutant that takes every error as a refusal (it would print `refused undefined`) survived. A new cli tester.test.mjs test throws from thread.claims with no code, then with `ECONNRESET`. Each rejects main with the same error and prints nothing. M9 (the code check dropped) is killed by that test. - **N2 (duckworth-0123), accepted.** Since 274, the owner's and a reader's thread.claims can include user threads (id, instance, holder run, harness, time; no name, no messages). That is the design join approved in duckworth-0118, with no owner filter, and owner-visible run governance under sage-1368 B1. - **N3 (duckworth-0123), for later.** When 264a and 277 meet, thread.claims's latest row per thread goes per session, and thread-claims.test.mjs needs the default session. It is checked in whichever round lands second. Gate (sage-2590): the touched test files plus user-principal, other-tree and host whole; the bus, cli, harness, business and seat suites; every scripts/test-*.sh; M1-M9. The kit prints HEAD and the manifest check first. Nothing runs before Sage's CLEAR.