researcher: row 142 round 2 packet, candidate rebased onto dda7e31b0 (#1636)

Row 145 added 3 lines to cli.mjs after the approved round 1 base; same change, new base, regenerated
candidate manifest, TOOLS.md line rewrapped. Every cli test file passes singly; 6/6 mutants killed on the new base.
Packet only; the candidate is row142.patch and is not applied to the tree. Nothing pushed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
2026-10-15 18:05:45 -05:00
co-authored by Claude Sonnet 5.5
parent d10a1a4677
commit 2118b3fdbb
51 changed files with 1030 additions and 0 deletions
@@ -0,0 +1,41 @@
# Row 142 (#1636) round 2: mosaic talk prints only the reply to its own message
Seat: Researcher. Reviewer: Filbert. Gate owner: Sage.
Brief: `docs/plans/2026-10-15_q167-north-star-rows.md`, section "mosaic talk prints only the reply to its own message".
Round 1: `../build.md`. Filbert approved round 1 on #1636 (comment 28627). This packet exists because the approved manifest no longer matches the landed base, and it carries one doc-only fix.
## Why round 2
Row 145's commit 467a313c7 added 3 lines to `packages/cli/src/cli.mjs` after my round 1 base (8e2c5c2d0). The landed `cli.mjs` blob therefore no longer equals the one Filbert approved. Nothing in my change moved; only the base did.
**Base: dda7e31b0** (detached worktree `/mnt/storage/scratch/researcher/q142r2`). Canonical HEAD was d5a9bf4c3 at packet time; `git diff --name-only dda7e31b0 HEAD -- packages roles scripts docs/TOOLS.md package.json` is empty (the commits after dda7e31b0 touch `BUILD-LOG.md`, `docs/` and `agents/` only).
`row142.patch` (this directory) is my diff on dda7e31b0: 4 files, +44 −24. Round 1's patch applied to dda7e31b0 with no conflict (`git apply --check` clean). It passes `git apply --cached --check` on dda7e31b0 and round-trips to the candidate tree `519d27488e29c5172264613e2e171679afcefcf7` (temp `GIT_INDEX_FILE`, trees equal). `candidate-manifest.sha256` lists the 4 files (sha256 of the manifest: `bb635c953d880719cea43712915378d579bb83d36d4b28669d587c2e9b95638c`). Secrets and binary scan of the patch: nothing. Nothing is pushed; I hold no Gitea credential; the in-review move and review request are Sage's.
## What differs from round 1
| File | Round 1 blob | Round 2 blob | Why |
|---|---|---|---|
| `packages/cli/src/cli.mjs` | `6a70d46c…` | `b1f269ce…` | the base now holds row 145's 3 lines; my hunks are the same two code hunks and one comment hunk as in round 1 |
| `docs/TOOLS.md` | `e3c2337e…` | `596e232a…` | one line rewrapped (below) |
| `packages/cli/README.md` | `666a8b14…` | `666a8b14…` | unchanged |
| `packages/cli/tests/verbs.test.mjs` | `d40868d4…` | `d40868d4…` | unchanged |
The code change is round 1's, line for line: in the `talk` receive loop, `if (!follow && m.in_reply_to !== sent.id) continue;` before the print and before `message.read`, and the timeout message ending "…and mosaic inbox shows it".
**Doc-only fix (Filbert's non-blocking note 3).** The `talk` sentence in `docs/TOOLS.md` had a line of 110 characters ("`--follow` and `follow` print and read everything. `stop` sends SIGTERM to a run's runner after checking it in"). I rewrapped it so no added line is longer than 75 characters. No wording changed.
## Verification
- **Every cli test file, one at a time** on the candidate tree (`cli-single/`, 19 files, all rc=0): app 30, boot-status 3, cli 12, config 6, follow 6, format 3, git-socket 18, host 41, launcher 17, launch-limits 1, limits 4, notifier 26, other-tree 4, stack-copy 12, tester 6, thread-launch 8, trackers-boot 1, transport 3, verbs 6. `verbs.test.mjs` (the three rewritten tests), `follow.test.mjs` and `tester.test.mjs` are among them. I ran all of them because the base moved under `cli.mjs` (rebase rule: run every touched test file singly; here, every file in the touched package).
- **Mutants rerun on the new base.** `mutants.sh` from round 1, unchanged, 2026-10-15T23:03:18Z to 23:03:43Z (`mutants-run.txt`, `mutants/`). Three controls (verbs, follow, tester) pass 6/6 each. **All 6 mutants fail their tests by assertion**: m1 and m2 and m3 and m4 fail 2 tests each, m5 and m6 fail 1 each, as in round 1. The worktree is restored (the candidate's 4 files are the only diff).
- **The full gate (cli suite and every `scripts/test-*.sh`) was not rerun for this packet.** Sage's instruction (sage-0406) is that the gate runs at landing, and the diff against round 1's gated tree is the base's own change plus a rewrapped doc line. I did run the whole cli suite's files singly, above. The row 140 gate I ran at 22:59 to 23:03Z on the same base dda7e31b0 was green (cli 208/208 with row 140's launcher test added, all ten `scripts/test-*.sh`), which says the base itself is healthy; it is not evidence for this candidate.
## Filbert's other non-blocking notes (not in this packet)
1. A message `talk` skips is delivered but stays unread, and no CLI verb marks it read. Sage queues a follow-up.
2. The Console's send text is stale (`bus.js:280` and `:261`, `send-browser.test.mjs:105`, `broker.mjs:1519`). That is a `public/` change needing decision 123; Sage queues a row.
## Not covered
- A live `mosaic talk` against a running bus; the evidence is the in-process broker tests, as in round 1.
- The carried row 132 notes (the `change_organization` "replaces" wording versus the broker's per-instance merge; a revision-only call refused by the writer, not the schema) are unrelated to this row.
@@ -0,0 +1,4 @@
596e232abf7f0258411bebbb042aeafbddffda8a9d626daee940da511d35b23f docs/TOOLS.md
666a8b14bacb3ab8e3dd56a4233b4d69ffb103d78797522e7bf2468bb658631f packages/cli/README.md
b1f269ceb4ba74cfc28649b9af2a39431f77b026a62e9a1b6da0805b90ebc779 packages/cli/src/cli.mjs
d40868d425d3cf79cd6cab0e98c1796365b398b93ef699d828977f0595576dcf packages/cli/tests/verbs.test.mjs
@@ -0,0 +1,4 @@
docs/TOOLS.md
packages/cli/README.md
packages/cli/src/cli.mjs
packages/cli/tests/verbs.test.mjs
@@ -0,0 +1,38 @@
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1333.47789ms)
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (65.211992ms)
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (147.248208ms)
✔ NO_COLOR: no escape codes, except reverse video on the selected row (227.62734ms)
✔ 1-9 shows the decide flow's own lines and question; only y resolves (58.888794ms)
✔ the app shows the same decide text as mosaic decide (27.186247ms)
✔ any key but y or Y at the question answers No; nothing is sent (230.930898ms)
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (69.163312ms)
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (292.176244ms)
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (37.268204ms)
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (23.002955ms)
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (415.099983ms)
✔ quitting at the question answers No and sends nothing, armed or not (43.590543ms)
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (40.140929ms)
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (55.655687ms)
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (355.422635ms)
✔ at every size the decide box shows each line y depends on, or says it is hidden (8438.175775ms)
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (73.541746ms)
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (32.5336ms)
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (16.823665ms)
✔ keys: views, moving, trail of a selection, the command line, help and quit (75.789528ms)
✔ one option: the status line says Press 1, not 1-1 (20.389821ms)
✔ no business: the app says so and :business picks one (9.680077ms)
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.785862ms)
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (165.265489ms)
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.440568ms)
✔ decide with --note still asks; a No sends nothing (19.257084ms)
✔ the app's business comes from a live bus host only (1.096705ms)
✔ runApp leaves the terminal as it found it on q, end of input, SIGTERM, SIGINT, SIGHUP and an error, the first frame's included (6.360622ms)
✔ draw is pure: same state, same frame (0.768973ms)
ℹ tests 30
ℹ suites 0
ℹ pass 30
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 12418.114575
@@ -0,0 +1,11 @@
✔ recordBootRefused keeps the last good load across refusals; recordBootOk clears the refusal (2.444491ms)
✔ an unknown key and a JSON syntax error each leave the bus down with the reason and path in the status (571.42467ms)
✔ a missing file is recorded too, and a valid file clears the status on the next start (388.879862ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1016.825297
@@ -0,0 +1,20 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (33.451583ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (23.808394ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (22.71354ms)
✔ decide prints a declining choice as declining (21.6896ms)
✔ an unknown outcome is reported once and never resent (20.416885ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (21.265239ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (21.302762ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (19.354872ms)
✔ every human command refuses inside an agent run before it touches the bus (18.876482ms)
✔ usage errors exit 4; no business and no host is a usage error (22.240944ms)
✔ agents and tasks print through the broker (20.199653ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.152742ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 406.621985
@@ -0,0 +1,14 @@
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (49.941734ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (95.478007ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (44.069607ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (50.294095ms)
✔ a business without tracker.baseUrl gets no trackers entry (43.739164ms)
✔ an unknown business and a broken system config refuse with exit 3 (72.009044ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 450.733356
@@ -0,0 +1,14 @@
✔ follow prints an unsolicited message after plain talk returned (31.850723ms)
✔ talk --follow continues after reply and prints a later unsolicited message (30.812436ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (288.965205ms)
✔ follow refuses agent context and bad arguments (10.510594ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (10.252411ms)
✔ plain talk leaves the process SIGINT handlers unchanged (11.92296ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 485.076193
@@ -0,0 +1,11 @@
✔ empty views say so (0.719789ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (0.908664ms)
✔ tasks print the tracker fields the snapshot carries (0.140949ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 51.181726
@@ -0,0 +1,26 @@
✔ the review loop: the coder pushes and asks, the reviewer fetches and approves, and the task's trail holds each step (309.725235ms)
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (334.045425ms)
✔ the size cap refuses a push before any byte (215.126952ms)
✔ a fetch over the cap refuses before any byte reaches the reviewer (466.782356ms)
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (187.617449ms)
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (252.446466ms)
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (572.132974ms)
✔ a review of a project the business no longer has refuses unknown-project (1543.242179ms)
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (1180.076229ms)
✔ close() answers host-closing to a connection still sending its request line (500.755754ms)
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (941.782205ms)
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (1835.179917ms)
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (1688.03596ms)
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (41.663116ms)
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (490.432593ms)
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (46.28593ms)
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (300.815754ms)
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (437.810899ms)
ℹ tests 18
ℹ suites 0
ℹ pass 18
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 11421.172149
@@ -0,0 +1,49 @@
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (869.755109ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (218.974197ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (100.4374ms)
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (515.28979ms)
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (11.33672ms)
✔ a host.lock that goes between the check and the read is tried again; one that can't be parsed still refuses as unreadable (0.881166ms)
✔ a live host's lock stays, however old the file is (0.600446ms)
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (811.977836ms)
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (215.335675ms)
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (28.454886ms)
✔ a broker fork that throws leaves no host.lock and no host.json (30.53204ms)
✔ a broker send that throws leaves no host.lock and no host.json (30.091519ms)
✔ a broker that misses its boot wait leaves no host.lock and no host.json (31.805049ms)
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (114.489774ms)
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (111.117979ms)
✔ a boot reply with no reader for the business refuses with exit 3 naming it, before the notifier starts, and leaves no host.lock and no live broker (124.983009ms)
✔ a boot reply with no reader for the business refuses with exit 3 naming it, with no notifier, and leaves no host.lock and no live broker (148.905879ms)
✔ a host.json that can't be written stops both children and leaves no host.lock (233.312477ms)
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (29.187883ms)
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (119.119789ms)
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (238.84428ms)
✔ an error event on the broker once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (222.502509ms)
✔ an error event on the notifier once the host is up stops the host with exit 1, and leaves no host.lock and no host.json (241.969367ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1116.093524ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (239.182308ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (133.115864ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (174.523607ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (163.762227ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (128.02145ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (197.216475ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (121.825221ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (173.274047ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (27.374654ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.602002ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (256.710813ms)
✔ bus start refuses with exit 3 without a notifier config (100.174306ms)
✔ bus start runs until bus stop; status reports it while it runs (695.96399ms)
✔ bus-service.sh renders the unit and installs it into a given directory (54.419758ms)
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (62.921712ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (12.411131ms)
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (7.879251ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 8418.323325
@@ -0,0 +1,9 @@
✔ each launch rereads limits and model selections; lowered limits keep sessions and log exact hit (19.524359ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 87.716373
@@ -0,0 +1,25 @@
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1213.854379ms)
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (434.44427ms)
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3695.197283ms)
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (188.512025ms)
✔ a runner that stops at once ends its launch with the runner's reason (269.137259ms)
✔ a Claude Code launch with no session credential refuses before it starts (198.929177ms)
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (566.472729ms)
✔ a runner killed from the host ends its launch as killed, not failed (381.585862ms)
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (218.189403ms)
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1307.216477ms)
✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (773.688204ms)
✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3712.867214ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (125.694338ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (126.291189ms)
✔ a launch client that never closes its side doesn't hold the host's close (131.778121ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1247.697928ms)
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (4.764445ms)
ℹ tests 17
ℹ suites 0
ℹ pass 17
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 14691.210843
@@ -0,0 +1,12 @@
✔ migration needs no bus/config, prints whole converted document, is idempotent and never writes (9.798812ms)
✔ limits view/set use the human bus transport and pass only the explicit patch (1.074269ms)
✔ post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses (166.674551ms)
✔ plugins view/set use the human bus transport and pass only the explicit patch (vikunja:19/305) (1.806578ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 281.848333
@@ -0,0 +1,34 @@
✔ zoned uses the IANA zone across DST (13.128415ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (18.297433ms)
✔ two blocking decisions get two DMs with different nonces (11.313253ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.18167ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (11.988508ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (13.957443ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (17.089441ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (11.262007ms)
✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (100.79942ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (53.784148ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (12.748208ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.222529ms)
✔ an inbox read failure is logged and the next poll retries (0.722967ms)
✔ no Discord id reaches the journal or the log (9.544532ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.789995ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (1.986084ms)
✔ the journal: a whole file that is one torn line truncates to empty (1.349357ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.508954ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.610828ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.047239ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.373784ms)
✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (1.769133ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.453007ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.504576ms)
✔ digest content stays within Discord's 2000 characters (0.293826ms)
✔ runLoop never overlaps ticks and stops after the one in flight (112.058864ms)
ℹ tests 26
ℹ suites 0
ℹ pass 26
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 472.016842
@@ -0,0 +1,12 @@
✔ a human command refused with human-required prints the other-tree line and exits 3 (2.248473ms)
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (0.766965ms)
✔ other exit-3 refusals print no other-tree line (1.121916ms)
✔ the line names no path (0.094905ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 110.203271
@@ -0,0 +1,20 @@
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (409.133119ms)
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (448.121908ms)
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (413.423783ms)
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (289.096861ms)
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (3502.556987ms)
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (527.809409ms)
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (192.911553ms)
✔ install never shows a template name on systemd 261; template drop-ins still refuse (166.120037ms)
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (159.729711ms)
✔ inherited mosaic-.service.d override refuses without changing the unit (184.237333ms)
✔ inherited service.d override refuses without changing the unit (189.773764ms)
✔ inherited drop-in unreadable directory or file refuses before writing (165.306297ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 6714.674219
@@ -0,0 +1,14 @@
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (36.558314ms)
✔ talk --tester refuses text without the [mos test] prefix before any call (10.624114ms)
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (15.390939ms)
✔ decide, stop and launches take no --tester (9.850701ms)
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.349571ms)
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.530893ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 181.862055
@@ -0,0 +1,16 @@
✔ REQ-THREAD-2 REQ-LAUNCH-1 REQ-MSG-1 a thread message starts the thread's session, which stops when idle; its history survives the stop and a restart and no other thread sees it (5472.805236ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a running thread counts against its model's limit and an idle-stopped one doesn't (1497.054319ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a turn longer than the idle time isn't stopped, and the thread idles out after it (4297.883757ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a message sent after the idle snapshot keeps the session, which answers it (2654.819621ms)
✔ REQ-ROLE-2 REQ-LAUNCH-1 a thread held by a run the launcher didn't start gets no second session (65.031254ms)
✔ REQ-LAUNCH-1 a run that ends without taking its thread's message isn't started again for that message (163.160795ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a thread id that isn't a safe segment, or a thread of a non-thread instance, launches nothing (189.448713ms)
✔ REQ-THREAD-1 REQ-LAUNCH-1 the chief can't launch the assistant outside a thread (53.663167ms)
ℹ tests 8
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 14493.405187
@@ -0,0 +1,11 @@
task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200
task.close on a missing task answered: task-not-found; it made GET /tasks/999 404
✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (256.821979ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 338.499011
@@ -0,0 +1,11 @@
✔ the transport writes {business, verb, args} to the child and reads its JSON (27.375078ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2130.09302ms)
✔ busExit and refuseInsideAgent (0.465303ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2217.91638
@@ -0,0 +1,14 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (44.150325ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1013.05731ms)
✔ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2023.370541ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (10.779154ms)
✔ launches off and on go to the broker and change the business's launch state (9.150946ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (8.720452ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3200.981313
@@ -0,0 +1 @@
2026-10-15T23:03:43Z
@@ -0,0 +1,26 @@
control-verbs: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0
control-follow: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0
control-tester: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0
m1-filter-removed: exit 1; ℹ tests 6 ℹ pass 4 ℹ fail 2
failed: ✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it
failed: ✖ failing tests:
failed: ✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread
m2-filter-under-follow: exit 1; ℹ tests 6 ℹ pass 4 ℹ fail 2
failed: ✖ failing tests:
failed: ✖ follow prints an unsolicited message after plain talk returned
failed: ✖ talk --follow continues after reply and prints a later unsolicited message
m3-filter-inverted: exit 1; ℹ tests 6 ℹ pass 4 ℹ fail 2
failed: ✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it
failed: ✖ failing tests:
failed: ✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread
m4-read-all: exit 1; ℹ tests 6 ℹ pass 4 ℹ fail 2
failed: ✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it
failed: ✖ failing tests:
failed: ✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread
m5-timeout-message: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1
failed: ✖ failing tests:
failed: ✖ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show
m6-any-reply: exit 1; ℹ tests 6 ℹ pass 5 ℹ fail 1
failed: ✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it
failed: ✖ failing tests:
mutants finished
@@ -0,0 +1 @@
2026-10-15T23:03:18Z
@@ -0,0 +1,14 @@
✔ follow prints an unsolicited message after plain talk returned (26.688565ms)
✔ talk --follow continues after reply and prints a later unsolicited message (23.726846ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (234.313038ms)
✔ follow refuses agent context and bad arguments (8.767604ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (8.717172ms)
✔ plain talk leaves the process SIGINT handlers unchanged (11.089982ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 401.069446
@@ -0,0 +1 @@
0
@@ -0,0 +1,14 @@
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (19.145161ms)
✔ talk --tester refuses text without the [mos test] prefix before any call (9.184681ms)
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (13.9626ms)
✔ decide, stop and launches take no --tester (9.051007ms)
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.32962ms)
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.452837ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 146.479696
@@ -0,0 +1 @@
0
@@ -0,0 +1,14 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (45.828297ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1013.994974ms)
✔ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2020.610299ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (11.873312ms)
✔ launches off and on go to the broker and change the business's launch state (9.81793ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.449056ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3200.715002
@@ -0,0 +1 @@
0
@@ -0,0 +1,44 @@
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (46.711835ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1017.733569ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2018.686765ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (11.834253ms)
✔ launches off and on go to the broker and change the business's launch state (9.697373ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.568ms)
ℹ tests 6
ℹ suites 0
ℹ pass 4
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3206.447421
✖ failing tests:
test at packages/cli/tests/verbs.test.mjs:32:1
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (46.711835ms)
AssertionError [ERR_ASSERTION]: another role's note and the same role's unsolicited note are not printed
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:59:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'sent e07943c2 to coder\ncto (INFO) 9060e278:\nfyi\ncoder (INFO) 6fbe31a5:\nside note\ncoder (RESULT, re e07943c2) ea084445:\ndone: 3 files\n',
expected: /fyi|side note/,
operator: 'doesNotMatch',
diff: 'simple'
}
test at packages/cli/tests/verbs.test.mjs:80:1
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2018.686765ms)
AssertionError [ERR_ASSERTION]: the late reply answers the first message, not this one
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:90:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'sent 7596dfc4 to coder\ncoder (RESULT, re 9cbe6e46) c0f7629b:\nlate answer\n',
expected: /late answer/,
operator: 'doesNotMatch',
diff: 'simple'
}
@@ -0,0 +1,10 @@
--- /mnt/storage/scratch/researcher/q142-mut2/.bak.5yX3T7/packages/cli/src/cli.mjs 2026-10-15 18:03:21.902792408 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:03:21.921024505 -0500
@@ -247,7 +247,6 @@
let answered = false;
for (const m of await call("message.receive")) {
// Row 142 (#1636): without --follow only the reply to the sent message is printed and marked read; the rest stays unread for mosaic inbox.
- if (!follow && m.in_reply_to !== sent.id) continue;
io.stdout.write(formatMessage(m));
await call("message.read", { id: m.id });
if (m.in_reply_to === sent?.id) answered = true;
@@ -0,0 +1 @@
1
@@ -0,0 +1,50 @@
✖ follow prints an unsolicited message after plain talk returned (24.439652ms)
✖ talk --follow continues after reply and prints a later unsolicited message (24.509387ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (255.750982ms)
✔ follow refuses agent context and bad arguments (9.940675ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (10.161946ms)
✔ plain talk leaves the process SIGINT handlers unchanged (11.993638ms)
ℹ tests 6
ℹ suites 0
ℹ pass 4
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 439.650935
✖ failing tests:
test at packages/cli/tests/follow.test.mjs:6:35
✖ follow prints an unsolicited message after plain talk returned (24.439652ms)
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /later unsolicited/. Input:
'sent 93d899e7 to coder\ncoder (INFO, re 93d899e7) b24fe4e9:\ndirect answer\n'
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/follow.test.mjs:22:33)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'sent 93d899e7 to coder\ncoder (INFO, re 93d899e7) b24fe4e9:\ndirect answer\n',
expected: /later unsolicited/,
operator: 'match',
diff: 'simple'
}
test at packages/cli/tests/follow.test.mjs:6:35
✖ talk --follow continues after reply and prints a later unsolicited message (24.509387ms)
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /later unsolicited/. Input:
'sent ab588637 to coder\ncoder (INFO, re ab588637) 72ff5102:\ndirect answer\n'
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/follow.test.mjs:22:33)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'sent ab588637 to coder\ncoder (INFO, re ab588637) 72ff5102:\ndirect answer\n',
expected: /later unsolicited/,
operator: 'match',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142-mut2/.bak.5yX3T7/packages/cli/src/cli.mjs 2026-10-15 18:03:21.902792408 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:03:25.179063866 -0500
@@ -247,7 +247,7 @@
let answered = false;
for (const m of await call("message.receive")) {
// Row 142 (#1636): without --follow only the reply to the sent message is printed and marked read; the rest stays unread for mosaic inbox.
- if (!follow && m.in_reply_to !== sent.id) continue;
+ if (m.in_reply_to !== sent?.id) continue;
io.stdout.write(formatMessage(m));
await call("message.read", { id: m.id });
if (m.in_reply_to === sent?.id) answered = true;
@@ -0,0 +1,47 @@
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (5020.632524ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1014.734724ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2037.99875ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (11.386095ms)
✔ launches off and on go to the broker and change the business's launch state (10.078677ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.040804ms)
ℹ tests 6
ℹ suites 0
ℹ pass 4
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 8216.410418
✖ failing tests:
test at packages/cli/tests/verbs.test.mjs:32:1
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (5020.632524ms)
AssertionError [ERR_ASSERTION]: no reply from coder within 5 s; it may still come, and mosaic inbox shows it
1 !== 0
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:54:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 1,
expected: 0,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/verbs.test.mjs:80:1
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2037.99875ms)
AssertionError [ERR_ASSERTION]: the late reply answers the first message, not this one
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:90:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'sent a11df7ff to coder\ncoder (RESULT, re 37afb926) d0f1faa3:\nlate answer\n',
expected: /late answer/,
operator: 'doesNotMatch',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142-mut2/.bak.5yX3T7/packages/cli/src/cli.mjs 2026-10-15 18:03:21.902792408 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:03:25.678945523 -0500
@@ -247,7 +247,7 @@
let answered = false;
for (const m of await call("message.receive")) {
// Row 142 (#1636): without --follow only the reply to the sent message is printed and marked read; the rest stays unread for mosaic inbox.
- if (!follow && m.in_reply_to !== sent.id) continue;
+ if (!follow && m.in_reply_to === sent.id) continue;
io.stdout.write(formatMessage(m));
await call("message.read", { id: m.id });
if (m.in_reply_to === sent?.id) answered = true;
@@ -0,0 +1,60 @@
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (45.477015ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1015.035ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2037.591589ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (11.294976ms)
✔ launches off and on go to the broker and change the business's launch state (10.948949ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.772441ms)
ℹ tests 6
ℹ suites 0
ℹ pass 4
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3222.156074
✖ failing tests:
test at packages/cli/tests/verbs.test.mjs:32:1
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (45.477015ms)
AssertionError [ERR_ASSERTION]: only the reply was marked read
3 !== 1
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:60:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 3,
expected: 1,
operator: 'strictEqual',
diff: 'simple'
}
test at packages/cli/tests/verbs.test.mjs:80:1
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2037.591589ms)
AssertionError [ERR_ASSERTION]: nothing was marked read
+ actual - expected
+ [
+ {
+ args: {
+ id: '505f7641-a41d-4d22-9ba9-672e6c2bc1ed'
+ },
+ business: 'demo',
+ verb: 'message.read'
+ }
+ ]
- []
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:91:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: [ { business: 'demo', verb: 'message.read', args: [Object] } ],
expected: [],
operator: 'deepStrictEqual',
diff: 'simple'
}
@@ -0,0 +1,12 @@
--- /mnt/storage/scratch/researcher/q142-mut2/.bak.5yX3T7/packages/cli/src/cli.mjs 2026-10-15 18:03:21.902792408 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:03:33.951169828 -0500
@@ -247,8 +247,7 @@
let answered = false;
for (const m of await call("message.receive")) {
// Row 142 (#1636): without --follow only the reply to the sent message is printed and marked read; the rest stays unread for mosaic inbox.
- if (!follow && m.in_reply_to !== sent.id) continue;
- io.stdout.write(formatMessage(m));
+ if (follow || m.in_reply_to === sent.id) io.stdout.write(formatMessage(m));
await call("message.read", { id: m.id });
if (m.in_reply_to === sent?.id) answered = true;
}
@@ -0,0 +1 @@
1
@@ -0,0 +1,35 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (46.167596ms)
✖ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1012.198975ms)
✔ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2043.008617ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (10.308845ms)
✔ launches off and on go to the broker and change the business's launch state (9.33835ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (8.636218ms)
ℹ tests 6
ℹ suites 0
ℹ pass 5
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3218.256892
✖ failing tests:
test at packages/cli/tests/verbs.test.mjs:69:1
✖ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1012.198975ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
+ actual - expected
+ 'no reply from coder within 1 s; it may still come, and the next mosaic talk shows it'
- 'no reply from coder within 1 s; it may still come, and mosaic inbox shows it'
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:77:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: true,
code: 'ERR_ASSERTION',
actual: 'no reply from coder within 1 s; it may still come, and the next mosaic talk shows it',
expected: 'no reply from coder within 1 s; it may still come, and mosaic inbox shows it',
operator: 'strictEqual',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142-mut2/.bak.5yX3T7/packages/cli/src/cli.mjs 2026-10-15 18:03:21.902792408 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:03:37.227209396 -0500
@@ -253,7 +253,7 @@
if (m.in_reply_to === sent?.id) answered = true;
}
if (!follow && answered) return;
- if (!follow && Date.now() >= deadline) throw new CliError(`no reply from ${to} within ${wait} s; it may still come, and mosaic inbox shows it`, 1);
+ if (!follow && Date.now() >= deadline) throw new CliError(`no reply from ${to} within ${wait} s; it may still come, and the next mosaic talk shows it`, 1);
if (controller.signal.aborted) break;
await new Promise(resolve => {
const wake = () => { clearTimeout(timer); controller.signal.removeEventListener("abort", wake); resolve(); };
@@ -0,0 +1,30 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (51.419701ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1013.362993ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2020.327917ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (11.561942ms)
✔ launches off and on go to the broker and change the business's launch state (9.687153ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.303763ms)
ℹ tests 6
ℹ suites 0
ℹ pass 5
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3204.806672
✖ failing tests:
test at packages/cli/tests/verbs.test.mjs:80:1
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2020.327917ms)
AssertionError [ERR_ASSERTION]: the late reply answers the first message, not this one
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r2/packages/cli/tests/verbs.test.mjs:90:10)
at async Test.run (node:internal/test_runner/test:1409:7)
at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) {
generatedMessage: false,
code: 'ERR_ASSERTION',
actual: 'sent 1b5de6ce to coder\ncoder (RESULT, re 6005114f) ab593680:\nlate answer\n',
expected: /late answer/,
operator: 'doesNotMatch',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142-mut2/.bak.5yX3T7/packages/cli/src/cli.mjs 2026-10-15 18:03:21.902792408 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:03:40.501248937 -0500
@@ -247,7 +247,7 @@
let answered = false;
for (const m of await call("message.receive")) {
// Row 142 (#1636): without --follow only the reply to the sent message is printed and marked read; the rest stays unread for mosaic inbox.
- if (!follow && m.in_reply_to !== sent.id) continue;
+ if (!follow && !m.in_reply_to) continue;
io.stdout.write(formatMessage(m));
await call("message.read", { id: m.id });
if (m.in_reply_to === sent?.id) answered = true;
@@ -0,0 +1 @@
1
@@ -0,0 +1,50 @@
12a7cd7d5828b03c9b469c9e0aad07f9e578582121b247e8a309e63bf00428e0 build.md
bb635c953d880719cea43712915378d579bb83d36d4b28669d587c2e9b95638c candidate-manifest.sha256
f2575be42ec9507bd02f1cf14e0f6437600bab330d61a709738b61d743d7f89b changed-paths.txt
d755095a9dbee204798fcf10f2ad69e50f29ad7427cc087bcc51bb77bf1ed533 cli-single/app.txt
145013058b948f406c7ab0cbd6f866976f54b304ab7feee1b16b93aa50ff7d3d cli-single/boot-status.txt
aebdbf3efcd1032a064a8d2f89bd3a303adc928b6570725ea228ae4e8ff96af4 cli-single/cli.txt
589e56182b8c47235e62d2eecdb7280a37eb7e14142dd93dd799b8e6ad42ec21 cli-single/config.txt
1fb3f3351337f456a04bfb36c61cd34c0f11dc9bc8ac4565a936c215af98ccf3 cli-single/follow.txt
9d31c4c34d48269df2ef295167fe2e14f86f11c0a8fc0580f97bf722c7a365fd cli-single/format.txt
6b2600d250fe4cb6c0fff2587ef7c9d2117ef4faa4ac2e70261f9d17332f3514 cli-single/git-socket.txt
5971c2fd2f8671fe8f4d34259d0f7fa61647936e49b3dfdba5fd6fb61f416798 cli-single/host.txt
d75aea0c5d93eb55954c1ab2b4ccdd06bd26300244f90a7404aeb0b2d0f4faf0 cli-single/launch-limits.txt
ab1576e9d4996836609f15b83f92aaec29904bf561d2e284575467658e72164a cli-single/launcher.txt
609cecf84f08bd4d24a60105ed974a79795fe3c4ca9af1f73282e1ddc0368df5 cli-single/limits.txt
6c1706d96763f6929a1664443e596a268e8d33e1701a639be16556b01bd95bc8 cli-single/notifier.txt
397b0071acda360cf65087e820708e8066a48222c0d95235f062da24ea280583 cli-single/other-tree.txt
93eb0579d1afdd4ffd20f6be95e3ce7fd85d8778a1b83b7f127a6f42ef9930c1 cli-single/stack-copy.txt
4b48dbe99ea7ea57a5dc503e654e87409837e07a65cddb25b9dd895f3ba9cb82 cli-single/tester.txt
1411ecd70ff1b7b3ecfb39500ec7c1d023fccef833611aa0ddf28dbf84cefaa0 cli-single/thread-launch.txt
26a6404f2ad2a79a9fe025f1f7671adc608f17adb0f2e0f4bc2c71680b78792f cli-single/trackers-boot.txt
38ff3e03db65d10d950b0ed9659e8ee56675f066a374b33a646f91662a82e387 cli-single/transport.txt
8137e5ee99e5beef94bf032b96799e74dd3f21829e5d499080f8d50b5cc503a3 cli-single/verbs.txt
a089c49f50f7583c6e61939313690514c2b0abd3a6009635883f245aa0c0d949 mutants-finished.txt
5be97a88610ff69247c28ff7042b60f6e13ac0c863233eacd9bed6b73a98cab7 mutants-run.txt
9622d27996c61bd30e5f5db0129376371f6a5f0c7190b643d42fddf2afcfcee3 mutants-started.txt
34aab402d360eaf611318937c34b1979332b2852ecb577e68dd6f3af5fbd2f71 mutants/control-follow.out
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-follow.rc
f4e442b6a29b1f6b250303e9589f9a108d6b99af945d4bf1990c17ba69f3e915 mutants/control-tester.out
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-tester.rc
c29c6fb546ef0ca7eeaa1e2f32547f08ef9fdab425fc1fc883df147195f2e74e mutants/control-verbs.out
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa mutants/control-verbs.rc
777ef2ef4973d119489f4485ea53bd85f5e7d8431feea635d00e29a16b497f4a mutants/m1-filter-removed.out
c21be65440d7b6c6a80b8632e64e86e582d613d722d4dafaff50dc8797c20823 mutants/m1-filter-removed.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m1-filter-removed.rc
54ea874b2463114af6d0a92d166846e7262cac4dad488539ba452c2a161eb330 mutants/m2-filter-under-follow.out
fb617154ee2c8f30ac89ca9a3eee30854356c6dff5f531cd8c49ae39fd6c0080 mutants/m2-filter-under-follow.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m2-filter-under-follow.rc
cf7d7a344dad5a41e4ee5ec4e0e1b1e6bf5eb4b5f9e4c4ac1b7ac67d879f25f7 mutants/m3-filter-inverted.out
37f3ee830fe9a50cf374330e0295e920ccfaf3e738a00000dc427c78d61aea7d mutants/m3-filter-inverted.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m3-filter-inverted.rc
d431fa787932fef5470b98fdbce856d357940b836f2c70d6172e6b1051417ea6 mutants/m4-read-all.out
d6cb163e82991ed3c13aa69b3c55547bb0a2bf370f1b48419623b446f67aa12e mutants/m4-read-all.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m4-read-all.rc
208098c5f8c056348997d7dbf79d75b93b3ca0a71918ff35a363369ec8d3017a mutants/m5-timeout-message.out
5889237a2a0512b71bdb456cf331dd5b45a6b2ce4fe0456f9ff12824dd19b62a mutants/m5-timeout-message.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m5-timeout-message.rc
0e244fe1f8db00d23ac5c903efa6332ebd39d39fe84d3c5bdb02c9e5a5ed5393 mutants/m6-any-reply.out
354505a923ee871fd60404e5e6376901271b604744d48bca6e3086c3708a4171 mutants/m6-any-reply.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 mutants/m6-any-reply.rc
9a01ae5996e144d10cb72e6d81826bc17fae210150fb2f9ec2167c8649dbaa08 row142.patch
@@ -0,0 +1,159 @@
diff --git a/docs/TOOLS.md b/docs/TOOLS.md
index baa06fad3..fa1edff42 100644
--- a/docs/TOOLS.md
+++ b/docs/TOOLS.md
@@ -262,9 +262,11 @@ unit's `MOSAIC_CLAUDE_BIN`, from `--claude PATH` or else `command -v claude`
PID namespace with a generated bundle, and `--pm` launches the business's
`launch.by` instance at start. A host that starts ends the launches a
crashed host left as `host-lost` and refuses with exit 3 on a
-`sessions.json` it can't read. `talk` sends a REQUEST and prints what
-arrives until the reply (polls every 2 s, `--wait` default 900; `0` only
-sends). `stop` sends SIGTERM to a run's runner after checking it in
+`sessions.json` it can't read. `talk` sends a REQUEST and prints only the
+reply to it, marking only that read (polls every 2 s, `--wait` default 900;
+`0` only sends); other messages stay unread for `mosaic inbox`, and
+`--follow` and `follow` print and read everything. `stop` sends SIGTERM to
+a run's runner after checking it in
`sessions.json`. `launches off|on` revokes and restores `role.launch` for
the business; running sessions keep running. `launches list` reads
`sessions.json` and needs no broker. `talk`, `stop` and `launches off|on`
diff --git a/packages/cli/README.md b/packages/cli/README.md
index da6877607..53bb2ab92 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -62,12 +62,13 @@ mosaic launches list [--json]
trail ends with its `task_ref` and `follow with: mosaic trail <task>`. It
does not pull in the task's rows.
- `talk` sends `<text>` to the role as a REQUEST, then polls the human's
- inbox every 2 s for up to `--wait` seconds (default 900). It prints every
- message that arrives and marks it read, and stops at the reply to its
- REQUEST. No reply in time exits 1; the reply may still come, and the next
- `mosaic talk` prints it. `--wait 0` only sends. `talk --follow` keeps
- printing and marking messages read until Ctrl-C, including messages with
- no `in_reply_to`. `follow` runs that same loop without sending. `--wait`
+ inbox every 2 s for up to `--wait` seconds (default 900). It prints the
+ reply to its REQUEST, marks only that read, and stops. Any other message
+ that arrives meanwhile is not printed and stays unread; `mosaic inbox`
+ lists it. No reply in time exits 1; the reply may still come, and
+ `mosaic inbox` shows it. `--wait 0` only sends. `talk --follow` keeps
+ printing and marking every message read until Ctrl-C, including messages
+ with no `in_reply_to`. `follow` runs that same loop without sending. `--wait`
and `--follow` together refuse. These commands consume the same human
inbox; run one receiving CLI at a time. Console only reads history.
- `launches off` and `on` are the broker's `launch.revoke` and
diff --git a/packages/cli/src/cli.mjs b/packages/cli/src/cli.mjs
index 60a0ba29b..dd8774175 100644
--- a/packages/cli/src/cli.mjs
+++ b/packages/cli/src/cli.mjs
@@ -213,8 +213,9 @@ function formatMessage(m) {
}
// `mosaic talk <role> <text>`: a REQUEST to the role, then the human's inbox
-// until the reply arrives or --wait runs out. --follow keeps receiving after
-// that reply; follow starts the same receive loop without sending.
+// until the reply to it arrives or --wait runs out; other messages stay unread.
+// --follow prints and reads every message and keeps receiving after that
+// reply; follow starts the same receive loop without sending.
async function talk(rest, io, deps, receiveOnly = false) {
const parsed = parse(rest, { flags: ["--follow", "--tester"], values: ["--business", ...(receiveOnly ? [] : ["--wait"])] });
if (parsed.positional.length !== (receiveOnly ? 0 : 2)) throw usage(receiveOnly ? "follow takes no arguments" : "talk takes a role and one quoted text");
@@ -245,12 +246,14 @@ async function talk(rest, io, deps, receiveOnly = false) {
while (!controller.signal.aborted) {
let answered = false;
for (const m of await call("message.receive")) {
+ // Row 142 (#1636): without --follow only the reply to the sent message is printed and marked read; the rest stays unread for mosaic inbox.
+ if (!follow && m.in_reply_to !== sent.id) continue;
io.stdout.write(formatMessage(m));
await call("message.read", { id: m.id });
if (m.in_reply_to === sent?.id) answered = true;
}
if (!follow && answered) return;
- if (!follow && Date.now() >= deadline) throw new CliError(`no reply from ${to} within ${wait} s; it may still come, and the next mosaic talk shows it`, 1);
+ if (!follow && Date.now() >= deadline) throw new CliError(`no reply from ${to} within ${wait} s; it may still come, and mosaic inbox shows it`, 1);
if (controller.signal.aborted) break;
await new Promise(resolve => {
const wake = () => { clearTimeout(timer); controller.signal.removeEventListener("abort", wake); resolve(); };
diff --git a/packages/cli/tests/verbs.test.mjs b/packages/cli/tests/verbs.test.mjs
index 6bdbc6793..b9842cdd7 100644
--- a/packages/cli/tests/verbs.test.mjs
+++ b/packages/cli/tests/verbs.test.mjs
@@ -28,32 +28,42 @@ function setup(t, deps = {}) {
const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args });
-test("talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply", async (t) => {
+// Row 142 (#1636): without --follow, talk prints and reads the reply to its own message and nothing else.
+test("talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread", async (t) => {
const s = setup(t);
const cto = s.b.bindLaunch({ business: "demo", role: "cto", run: "cto-run", harness: "pi" });
call(s.b, cto, "role.claim");
- // The coder answers on the second poll; a cto note arrives first and is printed too.
+ // The coder answers on the second poll; a cto note and an unsolicited coder note arrive first.
let polls = 0;
+ let note;
+ let unsolicited;
const transport = (business) => {
const inner = s.transport(business);
return async (verb, args) => {
if (verb === "message.receive" && ++polls === 2) {
const [m] = call(s.b, s.coder, "message.receive");
- call(s.b, cto, "message.send", { to: m.from_role, body: "fyi", class: "INFO" });
+ note = call(s.b, cto, "message.send", { to: m.from_role, body: "fyi", class: "INFO" });
+ unsolicited = call(s.b, s.coder, "message.send", { to: m.from_role, body: "side note", class: "INFO" });
call(s.b, s.coder, "message.send", { to: m.from_role, body: "done: 3 files", class: "RESULT", in_reply_to: m.id });
}
return inner(verb, args);
};
};
- const r = await s.run(["talk", "coder", "fix the build", "--business", "demo"], io(), { transport });
+ // A short --wait makes a reply that is never printed fail here instead of waiting out the default 900 s.
+ const r = await s.run(["talk", "coder", "fix the build", "--wait", "5", "--business", "demo"], io(), { transport });
assert.equal(r.code, 0, r.error);
const sent = s.calls.find((c) => c.verb === "message.send");
assert.deepEqual(sent.args, { to: "coder", body: "fix the build", class: "REQUEST" });
assert.match(r.io.out.text, /^sent [0-9a-f]{8} to coder\n/);
- assert.match(r.io.out.text, /cto \(INFO\) [0-9a-f]{8}:\nfyi\n/);
assert.match(r.io.out.text, /coder \(RESULT, re [0-9a-f]{8}\) [0-9a-f]{8}:\ndone: 3 files\n$/);
- assert.equal(s.calls.filter((c) => c.verb === "message.read").length, 2);
- assert.deepEqual(await s.transport("demo")("message.receive"), [], "both were marked read");
+ assert.doesNotMatch(r.io.out.text, /fyi|side note/, "another role's note and the same role's unsolicited note are not printed");
+ assert.equal(s.calls.filter((c) => c.verb === "message.read").length, 1, "only the reply was marked read");
+ // receive delivers a message once, so what talk skipped is no longer receivable; mosaic inbox lists it as unread.
+ const inbox = io();
+ await main(["inbox", "--business", "demo"], inbox, { system: { dataRoot: s.dataRoot }, transport: s.transport });
+ assert.match(inbox.out.text, /messages: 2 unread\n/, "the rest stays unread for mosaic inbox");
+ assert.match(inbox.out.text, new RegExp(`cto \\(INFO\\) ${note.id.slice(0, 8)}:\nfyi\n`));
+ assert.match(inbox.out.text, new RegExp(`coder \\(INFO\\) ${unsolicited.id.slice(0, 8)}:\nside note\n`));
});
test("talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show", async (t) => {
@@ -63,11 +73,11 @@ test("talk --wait 0 only sends; no reply within --wait exits 1 and says where it
assert.deepEqual(s.calls.map((c) => c.verb), ["message.send"]);
const slow = await s.run(["talk", "coder", "hello", "--wait", "1", "--business", "demo"]);
assert.equal(slow.code, 1);
- // Row 81 (#1575): only mosaic talk shows a late reply; mosaic inbox lists decisions, never messages.
- assert.equal(slow.error, "no reply from coder within 1 s; it may still come, and the next mosaic talk shows it");
+ // Row 142 (#1636): a late reply stays unread, so mosaic inbox lists it.
+ assert.equal(slow.error, "no reply from coder within 1 s; it may still come, and mosaic inbox shows it");
});
-test("a late reply the Console has listed still reaches the next talk", async (t) => {
+test("a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it", async (t) => {
const s = setup(t);
assert.equal((await s.run(["talk", "coder", "hello", "--wait", "1", "--business", "demo"])).code, 1);
const { id } = s.store.get("SELECT id FROM messages WHERE from_role='human' ORDER BY seq DESC LIMIT 1");
@@ -77,8 +87,12 @@ test("a late reply the Console has listed still reaches the next talk", async (t
assert.deepEqual(s.read("replies", { id }).map((m) => m.id), [answer.id]);
const next = await s.run(["talk", "coder", "again", "--wait", "1", "--business", "demo"]);
assert.equal(next.code, 1, "no answer to the second one");
- assert.match(next.io.out.text, new RegExp(`coder \\(RESULT, re ${id.slice(0, 8)}\\) ${answer.id.slice(0, 8)}:\nlate answer\n`));
- assert.deepEqual(s.calls.filter((c) => c.verb === "message.read").map((c) => c.args.id), [answer.id]);
+ assert.doesNotMatch(next.io.out.text, /late answer/, "the late reply answers the first message, not this one");
+ assert.deepEqual(s.calls.filter((c) => c.verb === "message.read"), [], "nothing was marked read");
+ const inbox = io();
+ await main(["inbox", "--business", "demo"], inbox, { system: { dataRoot: s.dataRoot }, transport: s.transport });
+ assert.match(inbox.out.text, /messages: 1 unread\n/);
+ assert.match(inbox.out.text, new RegExp(`coder \\(RESULT, re ${id.slice(0, 8)}\\) ${answer.id.slice(0, 8)}:\nlate answer\n`));
});
test("talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors", async (t) => {