morgana: #1782 design r1 (sage-2481): owner thread claims as a human/reader verb thread.claims, agents unchanged; Console and TUI read c.role, so the briefed mixed list would need them too (Q1, Q2 to Duckworth)

This commit is contained in:
jason.woltje committed 2026-10-22 00:11:28 -05:00
1 parent c27207f6e6
commit 3e9939b7b1
1 file changed
+151
@@ -0,0 +1,151 @@
# #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-<row> 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 <thread> <instance> <holder_run> <harness> since <at>
```
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.