Files
jason.woltje 4ff4f44c98 lead: commit researcher's untracked evidence (1406 files: queue-101 queue-105 queue-128 queue-134 queue-143 queue-148 queue-183 queue-228)
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.
2026-11-02 15:05:14 -06:00

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>] and mosaic project list [--json] call project.create and project.list. init and check keep their exact arguments and refusals; main sends create and list to projectBusCommand and everything else to the old projectCommand.
  • mosaic threads [--project <project>] [--json] prints <project>/<name> <instance> <short id>.
  • mosaic thread create <project> <name> prints id and instance; thread open <thread> calls thread.open; thread read <thread> pages thread.messages oldest first, marks nothing read, and prints with the same line inbox and talk use. Addition not in the brief: thread read --json, because threads and project list have it.
  • mosaic talk --thread <thread> <text> [--wait <s> | --follow] sends thread.send class REQUEST. Without --follow it prints only the message whose in_reply_to is 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 reads project.list and thread.list and refuses unknown, too-short and ambiguous references with exit 2 before any send, as findDecision does for decisions.
  • Every verb goes through callFor, so each refuses inside an agent run (exit 3) and takes --business.

Choices

  1. No default thread. Plain mosaic talk assistant <text> hits the broker's thread-required-use-thread-send; the CLI turns that code into one line naming mosaic talk --thread <thread> <text> and mosaic threads, exit 2. It never picks a thread (the brief asks the packet to record this).
  2. A full UUID the user's list doesn't show is passed to the broker. thread.list holds 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 own thread-not-owned (or thread-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" in thread-cli.test.mjs: the thread is owned by someone else, so talk --thread <id>, thread open <id>, thread read <id>, follow --thread <id> and talk --thread <id> --follow each print mosaic: 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 gives thread-not-found. Mutant r4 (drop the pass-through) fails it.
  3. Following a thread without a bus change. message.receive does not say which thread a message is in. --follow therefore receives, then asks thread.messages for the thread's ids after the last id it knows (threadMembers in thread.mjs), and prints and reads only a received message whose id is in that map and whose seq is greater than the sent message's seq (0 for follow --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 by message.receive but stays unread, which is where mosaic inbox finds it. The cost is one extra thread.messages call on each receive that returns something.
  4. A foreign thread is refused before any receive. First draft: the loop called message.receive and only then asked the thread, so follow --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. Mutant f4.
  5. --thread takes no --tester. The tester has no thread; exit 4.
  6. One positional. With --thread, talk takes the text only; talk --thread X assistant hello is 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 create arity). The test ran mosaic project create without --business, so callFor refused 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 hello was 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.