Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
12 KiB
Row 128 (#1622): mosaic talks to the assistant in a named thread
Owner: Researcher. Reviewer: Filbert. Gate owner: Sage.
Built 2026-10-18 in worktree /mnt/storage/scratch/researcher/r128-wt, detached at 0f3eb25ea
(brief docs/plans/2026-10-18_row128-thread-cli.md, blob e15cb12a92). Nothing on the manifest paths changed
between 0f3eb25ea and bf7d09c6c (git diff --stat is empty). No change to packages/bus.
Files: changed-paths.txt (8), hashes in candidate-manifest.sha256, full diff with the two new files in
row128.patch (638 insertions, 15 deletions).
What it does
mosaic project create <name> [--repo <repo>]andmosaic project list [--json]callproject.createandproject.list.initandcheckkeep their exact arguments and refusals;mainsendscreateandlisttoprojectBusCommandand everything else to the oldprojectCommand.mosaic threads [--project <project>] [--json]prints<project>/<name> <instance> <short id>.mosaic thread create <project> <name>prints id and instance;thread open <thread>callsthread.open;thread read <thread>pagesthread.messagesoldest first, marks nothing read, and prints with the same lineinboxandtalkuse. Addition not in the brief:thread read --json, becausethreadsandproject listhave it.mosaic talk --thread <thread> <text> [--wait <s> | --follow]sendsthread.sendclass REQUEST. Without--followit prints only the message whosein_reply_tois the sent id (row 142's rule, unchanged).mosaic follow --thread <thread>is the same loop without the send.- References: a project is its name or id; a thread is its id, an id prefix of 8 or more characters, or
<project>/<name>. The resolver readsproject.listandthread.listand refuses unknown, too-short and ambiguous references with exit 2 before any send, asfindDecisiondoes for decisions. - Every verb goes through
callFor, so each refuses inside an agent run (exit 3) and takes--business.
Choices
- No default thread. Plain
mosaic talk assistant <text>hits the broker'sthread-required-use-thread-send; the CLI turns that code into one line namingmosaic talk --thread <thread> <text>andmosaic threads, exit 2. It never picks a thread (the brief asks the packet to record this). - A full UUID the user's list doesn't show is passed to the broker.
thread.listholds only the caller's threads, so another user's thread id is not in it. If the resolver refused it as unknown, the output would say "no thread of yours" and REQ-USER-2 would be tested only by the resolver. Passing it on gets the broker's ownthread-not-owned(orthread-not-found). A name or a prefix is never passed on; it must be in the user's list. Covered by the test "REQ-USER-2 a thread of another user is refused for talk, open, read and follow, and nothing is written" inthread-cli.test.mjs: the thread is owned by someone else, sotalk --thread <id>,thread open <id>,thread read <id>,follow --thread <id>andtalk --thread <id> --followeach printmosaic: thread-not-owned, exit 2, and the counts of messages,thread_opens, deliveries,message_threads, threads and projects are unchanged. The same test checks that a name (theirs/private) and a prefix of that id are refused as unknown, and that a full id naming no thread givesthread-not-found. Mutantr4(drop the pass-through) fails it. - Following a thread without a bus change.
message.receivedoes not say which thread a message is in.--followtherefore receives, then asksthread.messagesfor the thread's ids after the last id it knows (threadMembersinthread.mjs), and prints and reads only a received message whose id is in that map and whoseseqis greater than the sent message'sseq(0 forfollow --thread, so it starts at the beginning of the thread). The broker stores a message's thread when it stores the message, so a received message is already listed. Everything else was delivered bymessage.receivebut stays unread, which is wheremosaic inboxfinds it. The cost is one extrathread.messagescall on each receive that returns something. - A foreign thread is refused before any receive. First draft: the loop called
message.receiveand only then asked the thread, sofollow --thread <another user's>would write delivery rows for every waiting message before it was refused. The loop now reads the thread once before the first receive; the broker's owner check fires there. The REQ-USER-2 test counts delivery rows with a message waiting. Mutantf4. --threadtakes no--tester. The tester has no thread; exit 4.- One positional. With
--thread,talktakes the text only;talk --thread X assistant hellois a usage error.
Tests
packages/cli/tests/thread-cli.test.mjs (11 tests) runs the real Broker on a real Store with the shipped
assistant's messaging lists. The transport raises what humanTransport raises (a CliError with the bus code and
busExit's exit code), so exit codes and the one-line refusals are the CLI's own. A run bound to a thread plays
the assistant; the chief plays a sender outside any thread.
| Test | Covers |
|---|---|
| project create and list | create, list, --json, no repo shows -, project-exists, unknown-repo, writes unchanged on refusal |
| thread create, list, open and read | create, list with and without --project, --json, thread-exists, unknown project, project by id, open writes one thread_opens row, read prints history and writes no delivery, arity refusals exit 4 |
| thread read pages | 205 messages come back oldest first |
| one threaded instance | thread-instance-missing and thread-instance-ambiguous (separate brokers), no write |
| resolver | id, 8-character prefix, 12-character prefix, <project>/<name>, too short, two ids sharing a prefix, unknown, no write |
| talk --thread | prints only the reply to the sent id; an earlier thread message, the chief's, another thread's and an unrelated thread message stay unread; class REQUEST |
| talk --thread --wait | no reply in time exits 1 with the message; --wait 0 sends and returns |
| --follow, follow --thread | only that thread's later messages printed and read; the earlier unread, thread B's and the chief's stay unread; follow opens and sends nothing |
| plain talk | exit 2 naming --thread and mosaic threads, no message stored; --thread arity and --tester and --follow --wait usage errors exit 4 |
| REQ-USER-2 | a thread owned by someone else: talk --thread, thread open, thread read, follow --thread, talk --thread --follow print nothing, exit 2 with thread-not-owned; message, thread_opens, delivery, message_threads, thread and project counts unchanged, with a message waiting for the user |
| agent run | every new verb exits 3 inside an agent run and writes nothing |
packages/cli/tests/project.test.mjs gains one test: create/list arity and option errors exit 4 before any bus
call, and list reaches project.list. The existing init/check tests are unchanged.
Mutants
mutants.sh (30 edits, each an exact-once text replacement; MUTANT_DRY=1 proves every edit matches once, and
it did on 2026-10-18). Controls run first. One mutant per behavior; the test file named is the one that must fail.
Results: see "Results" at the end of this file.
| Mutant | Behavior | Killing file |
|---|---|---|
| p1 | create passes --repo |
thread-cli |
| p2 | create prints the id | thread-cli |
| p3 | list shows - for no repo |
thread-cli |
| p4 | list --json |
thread-cli |
| p5 | create/list arity | project |
| p6 | init/check stay on the repository | project |
| r1 | prefix under 8 refused | thread-cli |
| r2 | ambiguous prefix refused | thread-cli |
| r3 | <project>/<name> resolves |
thread-cli |
| r4 | full id not in the list goes to the broker (REQ-USER-2) | thread-cli |
| r5 | prefix resolves | thread-cli |
| r6 | project by id | thread-cli |
| r7 | unknown project refused | thread-cli |
| v1 | create sends the project's id | thread-cli |
| v2 | open calls thread.open |
thread-cli |
| v3 | read pages | thread-cli |
| v4 | read --json |
thread-cli |
| v5 | threads --project |
thread-cli |
| v6 | threads --json |
thread-cli |
| v7 | threads arity | thread-cli |
| t1 | talk --thread sends thread.send |
thread-cli |
| t2 | class REQUEST | thread-cli |
| t3 | plain talk gets the --thread message |
thread-cli |
| t4 | only the reply is printed | thread-cli |
| t5 | --thread refuses --tester |
thread-cli |
| t6 | --thread takes one positional |
thread-cli |
| f1 | other threads' messages stay unread | thread-cli |
| f2 | earlier thread messages stay unread | thread-cli |
| f3 | the thread is re-read after a receive | thread-cli |
| f4 | foreign thread refused before any receive (REQ-USER-2) | thread-cli |
Out of scope, as the brief says
Renames, Console chat (row 129), closing or deleting threads, broker/schema/launcher changes, live smoke.
Row 129 will want the same resolver and the same seq filter; both are exported from thread.mjs, which
imports nothing from cli.mjs.
Results
Mutants
Run 1 (2026-10-18T14:17:44Z to 14:25:09Z; mutants-r1.log) found two weak tests:
- p5 survived (
project createarity). The test ranmosaic project createwithout--business, socallForrefused with exit 4 ("no bus host runs here, so name the business with --business") whether or not the arity check ran. Fixed: the test passes--business acme, so only the arity check can produce the 4, and the transport stub is the guard that nothing reached the bus. Re-run:mutants-r1b.log, killed. - t6 died only by the 400 s timeout (exit 124). With the mutant,
talk --thread X assistant hellowas accepted and waited the default 900 s for a reply. Fixed: that usage-error case now carries--wait 0, so a mutant that accepts it returns exit 0 at once. Re-run:mutants-r1b.log, killed by seven tests in 1 s.
Run 2 on the final candidate (14:26:05Z to 14:26:51Z; mutants-r2.log, mutants-r2-started.txt,
mutants-r2-finished.txt): both controls pass (thread-cli 11 of 11, project 4 of 4) and 30 of 30
mutants fail their named file, each with exit 1. Each mutant's patch is mutants-r2/<name>.patch. The
worktree matches the manifest afterwards (sha256sum -c) and no process of mine is left.
Existing CLI suite after both fixes: node --test packages/cli/tests/*.test.mjs is 242 of 242.
Full gate
Worktree matches the manifest (sha256sum -c, 0 non-OK). Base 0f3eb25ea; git diff 0f3eb25ea 3fd0b387f on the
eight manifest paths is empty. Compose project q128r1 with the network trap; networks-before.txt and
networks-after.txt are identical and no q128r1 container or network is left.
suites.sh first ran with a package list that stopped at ten (gate-r1.log, 14:27:00Z to 14:30:51Z); I noticed
the omission against the brief's "25 of 25", added control-board, conversation, discord, ledger and runs to the
script, and ran those five by hand with the same command (gate-r1b.log, outputs in gate-r1/<name>.txt). The
script in this packet has the full list; the combined result is what is below, not a single run of the script.
| Suite | Result |
|---|---|
| packages: project 37, bus 345, cli 242, webui 143, business 116, mosaic 69, plugins 28, tasks 97, harness 109, seat 150 | 10 of 10, 0 failed |
| packages: control-board 124, conversation 182, discord 183, ledger 86, runs 43 | 5 of 5, 0 failed |
| scripts: auth 15, conductor 17, config 24, discord 78, extension-package 18, foundation 44, land-work 86, queue 28, release 14, task 98 | 10 of 10, 0 failed |
That is 25 of 25. The queue package (156 tests) is not in the 25; I ran it too, once, and it passed.
cli is 242 of 242 here; the base count was not re-measured.