Files

15 KiB

#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:

[{ 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:

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.

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 <code>, 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.