researcher: row 142 round 3 packet, candidate rebased onto 277d1fa21 after row 146 (#1636)

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
jason.woltjeandClaude Sonnet 5.5 committed 2026-10-15 18:36:11 -05:00
1 parent e2a7cb38df
commit dcc2b29369
55 files changed
+1088

No files matched your search

@@ -0,0 +1,42 @@
# Row 142 (#1636) round 3: 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 2: `../r2/build.md`. Filbert approved round 2 (comment 28638). This packet exists because row 146 landed first (277d1fa21, pushed) and changed three of my four paths, so the approved manifest no longer matches the base. No behavior of mine changed.
## Base and candidate
**Base: 277d1fa21** (detached worktree `/mnt/storage/scratch/researcher/q142r3`). Canonical HEAD was e2a7cb38d at packet time; `git diff --name-only 277d1fa21 e2a7cb38d -- packages roles scripts docs/TOOLS.md package.json` is empty (the commits after 277d1fa21 touch docs and agents only).
`row142.patch` (this directory) is my diff on 277d1fa21: 4 files, +44 −24. It passes `git apply --cached --check` on 277d1fa21 and round-trips to the candidate tree `b994c1ce628fed20f54aca5dfebd7047fb8b4e1d` (temp `GIT_INDEX_FILE`, trees equal). `candidate-manifest.sha256` lists the 4 files (sha256 of the manifest: `c847d8fe411d1269bd751c595048d289f4a7c1f76f639b6acfe622e6d593dd81`). 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.
## How the rebase went
Round 2's patch applied with `git apply --3way` onto 277d1fa21:
| File | Result |
|---|---|
| `packages/cli/src/cli.mjs` | clean; 146 added `waitingLines` and one write after `sent`, my hunks are in the receive loop and don't overlap |
| `packages/cli/tests/verbs.test.mjs` | clean; 146's new test (the "isn't running" lines) sits beside my three rewritten tests |
| `docs/TOOLS.md` | clean; 146 didn't touch it, the file equals round 2's |
| `packages/cli/README.md` | **one conflict**, in the `talk` bullet. Resolved by hand, below |
**The one judgment call: the README `talk` bullet.** 146 inserted its "isn't running" text in the middle of the sentence my round changed; both rewrote the same lines. I kept 146's block word for word (from "When no run holds the role (row 146, #1640)," through "No verb launches `launch.by` on a running host.") and my sentences around it. The result reads: prints the reply to its REQUEST, marks only that read, and stops; any other message stays unread and `mosaic inbox` lists it; no reply in time exits 1 and `mosaic inbox` shows it; then 146's block; then `--wait 0` only sends; `--follow` prints and marks every message read. Two things I did not do: I did not rewrap 146's long line (“launches `launch.by` on a running host. `--wait 0` only sends. `talk --follow` keeps”, 86 characters with its indent) because the lines are 146's, and I changed "marking messages read" to "marking every message read" as in round 2.
## How the talk lines combine
In `talk`, 146's write is `if (sent.waiting) io.stdout.write(waitingLines(...))`, straight after `sent <id> to <role>` and before the receive loop. It prints once, from the broker's reply to `message.send`, and doesn't read the inbox. My filter is inside the loop (`if (!follow && m.in_reply_to !== sent.id) continue;`), so it decides only what the loop prints and marks read after those lines. Output order for a plain `talk` to a stopped role: `sent …`, 146's "isn't running" lines, then only the reply when it arrives (other messages skipped and left unread). The two don't share state, and neither reads the other's variable. With `--wait 0`, 146's lines print and `talk` returns before the loop, so the filter never runs. With `--follow` the filter is off and 146's lines still print once.
**Not covered by a test:** one `talk` that prints a 146 line, then receives a non-reply message and then the reply. 146's test uses a stopped role with no incoming messages, and mine use a running role. The two code paths don't touch, so I read this as a gap in the tests, not in the code, but it is not exercised.
## Verification
- **Every cli test file, one at a time**, plus 146's bus test, on the rebased candidate tree (`cli-single/`, 20 files, all rc=0, 2026-10-15T23:33:33Z to 23:34:41Z): 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 7, thread-launch 8, trackers-boot 1, transport 3, verbs 7, and `packages/bus/tests/launch-notice.test.mjs` 7. `verbs.test.mjs` (my three rewritten tests plus 146's), `follow.test.mjs`, `tester.test.mjs` (146 added one test, 6 to 7) and `launch-notice.test.mjs` are among them, as asked.
- **Mutants rerun on the new base.** `mutants.sh` from round 1, unchanged, 2026-10-15T23:34:50Z to 23:35:16Z (`mutants-run.txt`, `mutants/`). The dry run found each edit exactly once, so 146's change didn't move any line the mutants edit. Three controls (verbs 7/7, follow 6/6, tester 7/7) pass. **All 6 mutants fail their tests by assertion**: m1, m2, m3 and m4 fail 2 tests each, m5 and m6 fail 1 each, the same as round 2. The worktree is restored (the staged candidate is the only diff).
- **The full gate was not rerun for this packet.** Sage's rule is that the gate runs at landing. I ran the cli test files singly as above, not the whole cli suite in one process, and not the other packages' suites or `scripts/test-*.sh`. The packages outside `packages/cli` are untouched by this diff apart from the bus test I ran.
## Not in this packet
1. A message `talk` skips stays unread, and no CLI verb marks it read (Filbert round 1 note 1; 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`); a `public/` change needing decision 123.
3. A live `mosaic talk` against a running bus; the evidence is the in-process tests, as in rounds 1 and 2.
4. 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
2d5dac4b8edecd8fd6d28634863307d10c923b969ea0bff652681561e9169946 packages/cli/README.md
e5cb7c00a815832055210dcecfb3da43815b96b61bd5dc914bfc64fb2115fa8c packages/cli/src/cli.mjs
35ec58396a293d077974d4a561005b25889ad97d9a1cdd8dee56001aa89236db 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 @@
2026-10-15T23:34:41Z
@@ -0,0 +1 @@
2026-10-15T23:33:33Z
@@ -0,0 +1,20 @@
app rc=0 ℹ tests 30 ℹ pass 30 ℹ fail 0
boot-status rc=0 ℹ tests 3 ℹ pass 3 ℹ fail 0
cli rc=0 ℹ tests 12 ℹ pass 12 ℹ fail 0
config rc=0 ℹ tests 6 ℹ pass 6 ℹ fail 0
follow rc=0 ℹ tests 6 ℹ pass 6 ℹ fail 0
format rc=0 ℹ tests 3 ℹ pass 3 ℹ fail 0
git-socket rc=0 ℹ tests 18 ℹ pass 18 ℹ fail 0
host rc=0 ℹ tests 41 ℹ pass 41 ℹ fail 0
launcher rc=0 ℹ tests 17 ℹ pass 17 ℹ fail 0
launch-limits rc=0 ℹ tests 1 ℹ pass 1 ℹ fail 0
limits rc=0 ℹ tests 4 ℹ pass 4 ℹ fail 0
notifier rc=0 ℹ tests 26 ℹ pass 26 ℹ fail 0
other-tree rc=0 ℹ tests 4 ℹ pass 4 ℹ fail 0
stack-copy rc=0 ℹ tests 12 ℹ pass 12 ℹ fail 0
tester rc=0 ℹ tests 7 ℹ pass 7 ℹ fail 0
thread-launch rc=0 ℹ tests 8 ℹ pass 8 ℹ fail 0
trackers-boot rc=0 ℹ tests 1 ℹ pass 1 ℹ fail 0
transport rc=0 ℹ tests 3 ℹ pass 3 ℹ fail 0
verbs rc=0 ℹ tests 7 ℹ pass 7 ℹ fail 0
launch-notice rc=0 ℹ tests 7 ℹ pass 7 ℹ fail 0
@@ -0,0 +1,38 @@
✔ every screen and overlay fits 120x36, 80x24 and the narrow layout, row for row (1138.029948ms)
✔ width counts terminal cells: wide CJK and emoji 2, U+FE0F after a one-cell base 1, marks 0 (63.364477ms)
✔ broker text is shown, never interpreted: no raw control character or bidi override reaches the terminal (168.486358ms)
✔ NO_COLOR: no escape codes, except reverse video on the selected row (245.73723ms)
✔ 1-9 shows the decide flow's own lines and question; only y resolves (80.012229ms)
✔ the app shows the same decide text as mosaic decide (24.965681ms)
✔ any key but y or Y at the question answers No; nothing is sent (215.866749ms)
✔ below 40x10 the question is hidden, so every key answers it No, y included (Filbert R3) (66.25737ms)
✔ under 40 columns a tall terminal shows the too-small screen even where the decide box would fit; y sends nothing (Filbert N10) (249.620349ms)
✔ a hidden decide box names the way out, and the status line under it does not promise y (Filbert N12) (30.466204ms)
✔ a throw while answering the question goes to crash, sends nothing and leaves no job pending (21.336628ms)
✔ type-ahead never answers the question: a paste, keys typed before it is drawn, keys inside askMs (398.357306ms)
✔ quitting at the question answers No and sends nothing, armed or not (33.053456ms)
✔ a timer tick reads nothing under an overlay, never answers the question, and keeps a resolved status (41.050433ms)
✔ a long question is cut inside the question; the action, target, choice and prompt stay (Dewey B1) (52.443471ms)
✔ a decide box too big for the terminal says so, and every key answers it No, y included (Dewey B1, round 3) (357.179906ms)
✔ at every size the decide box shows each line y depends on, or says it is hidden (8139.378536ms)
✔ outcome unknown: the CLI's sentence, no retry, then the trail or a re-read (72.989997ms)
✔ refused and failed reads keep the last list, marked stale; a refused decide says refused (32.93088ms)
✔ help or a detail box cut to fit keeps its last line, Esc included (Dewey) (16.105817ms)
✔ keys: views, moving, trail of a selection, the command line, help and quit (84.554355ms)
✔ one option: the status line says Press 1, not 1-1 (20.508807ms)
✔ no business: the app says so and :business picks one (11.510233ms)
✔ mosaic with no argument: exit 3 in an agent run, exit 4 with the usage off a terminal, the app on one (1.799516ms)
✔ scripts/mosaic with no argument reaches the app entry; the seat usage it printed before has not drifted (170.918328ms)
✔ strings to keep exactly live once, in the decide flow and the transport; the app does not restate them (0.431208ms)
✔ decide with --note still asks; a No sends nothing (10.924221ms)
✔ the app's business comes from a live bus host only (0.986817ms)
✔ 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.843382ms)
✔ draw is pure: same state, same frame (0.770893ms)
ℹ tests 30
ℹ suites 0
ℹ pass 30
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 11857.874206
@@ -0,0 +1,11 @@
✔ recordBootRefused keeps the last good load across refusals; recordBootOk clears the refusal (2.866596ms)
✔ an unknown key and a JSON syntax error each leave the bus down with the reason and path in the status (570.943939ms)
✔ a missing file is recorded too, and a valid file clears the status on the next start (390.43615ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1020.459439
@@ -0,0 +1,20 @@
✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (16.327839ms)
✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (12.656791ms)
✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (10.688425ms)
✔ decide prints a declining choice as declining (10.071129ms)
✔ an unknown outcome is reported once and never resent (8.540942ms)
✔ a decision closed before the answer arrives exits 2 and points at its trail (8.424674ms)
✔ a prefix that matches two open decisions exits 2 and resolves neither (8.356609ms)
✔ without --business a command uses the live host's business, and a stale host.json is not a host (8.0382ms)
✔ every human command refuses inside an agent run before it touches the bus (8.158002ms)
✔ usage errors exit 4; no business and no host is a usage error (8.916297ms)
✔ agents and tasks print through the broker (7.887602ms)
✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.032277ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 202.741992
@@ -0,0 +1,14 @@
✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (31.248159ms)
✔ trackers come from the tracker.* variables of the one project that names a tracker project (52.218605ms)
✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (26.920938ms)
✔ two projects that each name a tracker project refuse, since the boot shape holds one (27.774001ms)
✔ a business without tracker.baseUrl gets no trackers entry (25.697052ms)
✔ an unknown business and a broken system config refuse with exit 3 (50.29011ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 270.457924
@@ -0,0 +1,14 @@
✔ follow prints an unsolicited message after plain talk returned (27.069745ms)
✔ talk --follow continues after reply and prints a later unsolicited message (24.392551ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (231.514766ms)
✔ follow refuses agent context and bad arguments (8.470661ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (9.084015ms)
✔ plain talk leaves the process SIGINT handlers unchanged (12.19726ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 401.323681
@@ -0,0 +1,11 @@
✔ empty views say so (0.592553ms)
✔ the trail keeps the broker's order and names a decision's task without its rows (0.724726ms)
✔ tasks print the tracker fields the snapshot carries (0.126626ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 41.337551
@@ -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 (302.586719ms)
✔ refusals: no action, non-fast-forward, an unknown commit or request, and a push for another task moves only that task's ref (297.653766ms)
✔ the size cap refuses a push before any byte (158.747322ms)
✔ a fetch over the cap refuses before any byte reaches the reviewer (482.9091ms)
✔ the socket refuses a request with no capability, an unknown one, and a bad request line (206.244684ms)
✔ a rigged clone runs nothing: hooks, fsmonitor, a filter, a gitlink and a symlinked .git/config leave no marker (283.83059ms)
✔ an identity with no role, or of another business, refuses not-a-role and moves no ref (199.6483ms)
✔ a review of a project the business no longer has refuses unknown-project (193.090704ms)
✔ past maxActive connections the socket refuses git-busy, and a hung-up connection frees its slot (139.902764ms)
✔ close() answers host-closing to a connection still sending its request line (143.379932ms)
✔ a bundle that trickles in refuses body-timeout at its deadline and frees its slot (547.658902ms)
✔ a reviewer gone before its bundle was sent is filed fetch-aborted, not as the host's fault (414.826088ms)
✔ a reviewer that hangs up the moment it has the whole bundle was served, not aborted (494.95687ms)
✔ send: a close after every byte left is served; one that fails a queued write is fetch-aborted (42.85415ms)
✔ a bundle with no bytes for bodyIdle refuses body-timeout long before its deadline (496.314969ms)
✔ send: a close once the loop is past its last chunk is served, and a failed write there is fetch-aborted (43.946846ms)
✔ letGo waits for an answer to leave for at most its cap, then destroys the socket (301.538625ms)
✔ a record that fails and can't be undone refuses rollback-failed, which the host logs (378.450669ms)
ℹ tests 18
ℹ suites 0
ℹ pass 18
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5192.157613
@@ -0,0 +1,49 @@
✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (875.551151ms)
✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (183.79441ms)
✔ a second host for the same data root refuses with exit 3 while the first runs (97.559489ms)
✔ two bus starts racing for one data root leave one host running; the other refuses that a host already runs (497.888836ms)
✔ a host takes over a host.lock its dead host left, and puts back a live claim that replaced it meanwhile (1.828643ms)
✔ 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.71451ms)
✔ a live host's lock stays, however old the file is (0.48231ms)
✔ a host.lock left by a host killed with SIGKILL doesn't stop the next bus start (753.137918ms)
✔ while a host boots, a reader sees no host: the human commands still ask for --business, stop and status find none (190.772558ms)
✔ a host started before host.lock existed, holding only host.json, still refuses a second start, which leaves no lock (26.002234ms)
✔ a broker fork that throws leaves no host.lock and no host.json (29.548756ms)
✔ a broker send that throws leaves no host.lock and no host.json (27.113303ms)
✔ a broker that misses its boot wait leaves no host.lock and no host.json (27.843586ms)
✔ a notifier fork that throws stops the broker and leaves no host.lock and no host.json (100.743246ms)
✔ a notifier send that throws stops the broker and leaves no host.lock and no host.json (96.947173ms)
✔ 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 (97.291642ms)
✔ 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 (99.9561ms)
✔ a host.json that can't be written stops both children and leaves no host.lock (192.813876ms)
✔ an error event on the broker before it replies fails the start at once, and leaves no host.lock and no live child (29.531654ms)
✔ an error event on the notifier before it replies fails the start at once, and leaves no host.lock and no live child (99.952657ms)
✔ an error event on the broker while the notifier starts fails the start once the notifier replies (194.291055ms)
✔ 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 (191.575313ms)
✔ 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 (198.938262ms)
✔ a broker reply that misses the wait breaks the channel: the late reply answers nothing, later requests refuse, the host exits 1 (1110.477547ms)
✔ a broker reply with another request's id, or none, breaks the channel and the host exits 1 (211.968587ms)
✔ a broker reply with no request waiting breaks the channel and the host exits 1 (100.011101ms)
✔ a notifier that refuses stops the broker and the host refuses with exit 3 (143.800311ms)
✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (137.790907ms)
✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (97.019085ms)
✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (148.152006ms)
✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (99.020819ms)
✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (156.660939ms)
✔ watchChildren reports a child that died before it was called, and one that dies later (25.014732ms)
✔ bus stop refuses to signal a live pid that is not a bus host (202.484401ms)
✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (257.444083ms)
✔ bus start refuses with exit 3 without a notifier config (100.878335ms)
✔ bus start runs until bus stop; status reports it while it runs (688.891561ms)
✔ bus-service.sh renders the unit and installs it into a given directory (50.185585ms)
✔ bus-service.sh pins the claude binary, refuses one the unit can't carry, and renders without one (61.946037ms)
✔ bus-service.sh refuses a directory pin and keeps a symlink default unresolved (12.484743ms)
✔ bus-service.sh refuses a default claude without the execute bit, or in a PATH dir with a space (7.890283ms)
ℹ tests 41
ℹ suites 0
ℹ pass 41
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 7718.65884
@@ -0,0 +1,9 @@
✔ each launch rereads limits and model selections; lowered limits keep sessions and log exact hit (18.10327ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 78.087178
@@ -0,0 +1,15 @@
✔ a message to a stopped role sends launch.by one notice, and the result says so (18.511812ms)
✔ a message to a running role sends no notice and its result is unchanged (8.823176ms)
✔ a re-route at a claim, a release and claim, and a broker restart send no second notice (15.067864ms)
✔ a run launched but not yet claimed counts as no holder (7.688456ms)
✔ no notice for a message to launch.by, from it, from a tester, to the human, or in a thread to a thread instance (17.168849ms)
✔ a business with no launch block tells nobody and says there is no launcher (7.221084ms)
✔ the notice sender stays internal: no role is named _bus and no caller can send as it or to it (22.698649ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 151.58011
@@ -0,0 +1,25 @@
✔ bus start --pm: the launcher instance (the chief of staff) runs under its own PID namespace, registered, with a manifest (1209.112019ms)
✔ the chief of staff launches a coder through its launch tool; the coder answers; refusals name their code (434.316151ms)
✔ each launch makes sure the workspace has a clone of the project: cloned, fast-forwarded, or fetched and logged (3588.603465ms)
✔ a project clone that can't be made refuses checkout-failed, from --chief and from a session, with no run and no clone left (134.020004ms)
✔ a runner that stops at once ends its launch with the runner's reason (208.633432ms)
✔ a Claude Code launch with no session credential refuses before it starts (110.858761ms)
✔ a pi launch needs no session credential: pi logs in through its own ENV_ALLOW variables (195.016112ms)
✔ a runner killed from the host ends its launch as killed, not failed (251.980527ms)
✔ a missing or failing bwrap refuses sandbox-unavailable before anything starts; there is no unconfined fallback (118.299307ms)
✔ a real Claude Code coder's shell can't read a stand-in secret or anything else its sandbox doesn't hold (1021.540397ms)
✔ 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 (681.418159ms)
✔ 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 (3778.521621ms)
✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (124.981016ms)
✔ an over-long launch request is refused at once, not at the 10 s idle timeout (110.706554ms)
✔ a launch client that never closes its side doesn't hold the host's close (111.724691ms)
✔ a runner that ignores SIGTERM is killed when the host closes (1227.688994ms)
✔ claudeVersion asks MOSAIC_CLAUDE_BIN when set, else claude on PATH (4.231697ms)
ℹ tests 17
ℹ suites 0
ℹ pass 17
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 13397.211208
@@ -0,0 +1,12 @@
✔ migration needs no bus/config, prints whole converted document, is idempotent and never writes (8.848874ms)
✔ limits view/set use the human bus transport and pass only the explicit patch (0.931784ms)
✔ post-119 example and Jason fixture migrate with checked inherited models; wrong or absent config refuses (151.36641ms)
✔ plugins view/set use the human bus transport and pass only the explicit patch (vikunja:19/305) (1.895546ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 253.536053
@@ -0,0 +1,34 @@
✔ zoned uses the IANA zone across DST (11.273354ms)
✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (18.526674ms)
✔ two blocking decisions get two DMs with different nonces (11.393968ms)
✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.158361ms)
✔ a failed DM is journaled, backs off, and is retried until it lands (11.271207ms)
✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (12.795134ms)
✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (15.413661ms)
✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (10.905457ms)
✔ 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 (85.488037ms)
✔ a restart after the second refusal does not send before that refusal's 30 min are up (44.269797ms)
✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (13.488337ms)
✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (9.387178ms)
✔ an inbox read failure is logged and the next poll retries (0.628612ms)
✔ no Discord id reaches the journal or the log (9.131723ms)
✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (1.632455ms)
✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (1.823829ms)
✔ the journal: a whole file that is one torn line truncates to empty (1.068061ms)
✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.416244ms)
✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.51476ms)
✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.712323ms)
✔ the journal: a symlinked directory refuses and says it is a link (0.288278ms)
✔ 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.493029ms)
✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.40477ms)
✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.465041ms)
✔ digest content stays within Discord's 2000 characters (0.237739ms)
✔ runLoop never overlaps ticks and stops after the one in flight (110.715635ms)
ℹ tests 26
ℹ suites 0
ℹ pass 26
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 433.115031
@@ -0,0 +1,12 @@
✔ a human command refused with human-required prints the other-tree line and exits 3 (1.962344ms)
✔ a --tester command refused with tester-required prints the other-tree line and exits 3 (0.725068ms)
✔ other exit-3 refusals print no other-tree line (1.020297ms)
✔ the line names no path (0.083734ms)
ℹ tests 4
ℹ suites 0
ℹ pass 4
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 95.813032
@@ -0,0 +1,20 @@
✔ shared copy: npm ci, pushed reachability, readonly/existing copy, webui delegation (359.658369ms)
✔ wrapper preserves Q81 lease and marker, rejects foreign/symlink; check uses effective roots (383.836243ms)
✔ bus unit copy/pm pins, install refuses all busy states and failed reads, drop-in refusal (385.976249ms)
✔ missing package manifest refuses before npm; npm failure removes the unpublished temp copy (197.754427ms)
✔ real pinned install loads bus and Console; same-copy mosaic talk passes proof, other trees refuse (3139.160106ms)
✔ bus status reads effective copy/pm/drop-ins without writes; check refuses an overridden ExecStart (440.407833ms)
✔ drop-ins in either override field and malformed metadata refuse; uninstall also checks --no-reload (164.200605ms)
✔ install never shows a template name on systemd 261; template drop-ins still refuse (160.014177ms)
✔ unlisted instance drop-in is found in another unit lookup directory; path discovery errors refuse (146.51465ms)
✔ inherited mosaic-.service.d override refuses without changing the unit (169.135013ms)
✔ inherited service.d override refuses without changing the unit (163.870771ms)
✔ inherited drop-in unreadable directory or file refuses before writing (142.394597ms)
ℹ tests 12
ℹ suites 0
ℹ pass 12
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 5913.362736
@@ -0,0 +1,15 @@
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (32.55221ms)
✔ talk --tester refuses text without the [mos test] prefix before any call (9.474302ms)
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (13.692908ms)
✔ decide, stop and launches take no --tester (8.950635ms)
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.274272ms)
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.448748ms)
✔ talk --tester to a stopped role says nobody is told to launch it (8.82473ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 164.847665
@@ -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 (5366.844504ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a running thread counts against its model's limit and an idle-stopped one doesn't (1494.580394ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a turn longer than the idle time isn't stopped, and the thread idles out after it (4307.597751ms)
✔ REQ-THREAD-2 REQ-LAUNCH-1 a message sent after the idle snapshot keeps the session, which answers it (2657.005523ms)
✔ REQ-ROLE-2 REQ-LAUNCH-1 a thread held by a run the launcher didn't start gets no second session (48.939172ms)
✔ REQ-LAUNCH-1 a run that ends without taking its thread's message isn't started again for that message (147.087929ms)
✔ 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 (176.519814ms)
✔ REQ-THREAD-1 REQ-LAUNCH-1 the chief can't launch the assistant outside a thread (47.433405ms)
ℹ tests 8
ℹ suites 0
ℹ pass 8
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 14332.635416
@@ -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 (246.676804ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 320.030805
@@ -0,0 +1,11 @@
✔ the transport writes {business, verb, args} to the child and reads its JSON (25.476782ms)
✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2115.950153ms)
✔ busExit and refuseInsideAgent (0.454886ms)
ℹ tests 3
ℹ suites 0
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2196.737338
@@ -0,0 +1,15 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (43.445515ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1014.442165ms)
✔ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2020.697922ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (26.113153ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (10.09901ms)
✔ launches off and on go to the broker and change the business's launch state (10.154323ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.14568ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3224.40668
@@ -0,0 +1 @@
2026-10-15T23:35:16Z
@@ -0,0 +1,26 @@
control-verbs: exit 0; ℹ tests 7 ℹ pass 7 ℹ fail 0
control-follow: exit 0; ℹ tests 6 ℹ pass 6 ℹ fail 0
control-tester: exit 0; ℹ tests 7 ℹ pass 7 ℹ fail 0
m1-filter-removed: exit 1; ℹ tests 7 ℹ pass 5 ℹ 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 7 ℹ pass 5 ℹ 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 7 ℹ pass 5 ℹ 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 7 ℹ pass 6 ℹ 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 7 ℹ pass 6 ℹ 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:34:50Z
@@ -0,0 +1,14 @@
✔ follow prints an unsolicited message after plain talk returned (27.776183ms)
✔ talk --follow continues after reply and prints a later unsolicited message (26.569004ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (255.842372ms)
✔ follow refuses agent context and bad arguments (9.759566ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (10.261036ms)
✔ plain talk leaves the process SIGINT handlers unchanged (15.462067ms)
ℹ tests 6
ℹ suites 0
ℹ pass 6
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 436.14029
@@ -0,0 +1 @@
0
@@ -0,0 +1,15 @@
✔ talk --tester sends a [mos test] message as tester from inside an agent and prints the role's reply (21.692997ms)
✔ talk --tester refuses text without the [mos test] prefix before any call (10.600734ms)
✔ the read verbs take --tester inside an agent, and the trail shows the tester as the sender (15.494548ms)
✔ decide, stop and launches take no --tester (9.355608ms)
✔ the tester transport runs tester-cli.mjs, and the tester's refusals exit 3 (0.41269ms)
✔ with no stand-in transport, --tester runs tester-cli.mjs on the data root's socket (0.486405ms)
✔ talk --tester to a stopped role says nobody is told to launch it (8.991928ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 166.979474
@@ -0,0 +1 @@
0
@@ -0,0 +1,15 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (44.532172ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1031.407364ms)
✔ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2022.146666ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (25.494073ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (9.292521ms)
✔ launches off and on go to the broker and change the business's launch state (8.95137ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (8.660745ms)
ℹ tests 7
ℹ suites 0
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3240.343769
@@ -0,0 +1 @@
0
@@ -0,0 +1,45 @@
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (46.275869ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1019.394855ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2033.827473ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (24.789435ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (9.569817ms)
✔ launches off and on go to the broker and change the business's launch state (9.321148ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.214968ms)
ℹ tests 7
ℹ suites 0
ℹ pass 5
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3262.66688
✖ 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.275869ms)
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/q142r3/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 dba0521f to coder\ncto (INFO) 3bf94def:\nfyi\ncoder (INFO) 4fa59e8d:\nside note\ncoder (RESULT, re dba0521f) 793e758d:\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 (2033.827473ms)
AssertionError [ERR_ASSERTION]: the late reply answers the first message, not this one
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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 bd8a4bca to coder\ncoder (RESULT, re 055a6bae) 5f05304e:\nlate answer\n',
expected: /late answer/,
operator: 'doesNotMatch',
diff: 'simple'
}
@@ -0,0 +1,10 @@
--- /mnt/storage/scratch/researcher/q142r3-out/mutants/.bak.iUhyT5/packages/cli/src/cli.mjs 2026-10-15 18:34:54.210904265 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:34:54.238807195 -0500
@@ -260,7 +260,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.431827ms)
✖ talk --follow continues after reply and prints a later unsolicited message (22.255727ms)
✔ inbox lists all unread human messages under decisions, does not deliver or read them (228.921529ms)
✔ follow refuses agent context and bad arguments (8.90128ms)
✔ follow Ctrl-C wakes the poll sleep and removes its handler; transport errors also clean up (8.811205ms)
✔ plain talk leaves the process SIGINT handlers unchanged (11.865648ms)
ℹ tests 6
ℹ suites 0
ℹ pass 4
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 394.662336
✖ failing tests:
test at packages/cli/tests/follow.test.mjs:6:35
✖ follow prints an unsolicited message after plain talk returned (24.431827ms)
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /later unsolicited/. Input:
'sent 4dd4968e to coder\ncoder (INFO, re 4dd4968e) b9683d8d:\ndirect answer\n'
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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 4dd4968e to coder\ncoder (INFO, re 4dd4968e) b9683d8d:\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 (22.255727ms)
AssertionError [ERR_ASSERTION]: The input did not match the regular expression /later unsolicited/. Input:
'sent d0242c32 to coder\ncoder (INFO, re d0242c32) e4a2ab77:\ndirect answer\n'
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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 d0242c32 to coder\ncoder (INFO, re d0242c32) e4a2ab77:\ndirect answer\n',
expected: /later unsolicited/,
operator: 'match',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142r3-out/mutants/.bak.iUhyT5/packages/cli/src/cli.mjs 2026-10-15 18:34:54.210904265 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:34:57.553846130 -0500
@@ -260,7 +260,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,48 @@
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (5036.954429ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1016.625683ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2036.628037ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (25.060106ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (9.026752ms)
✔ launches off and on go to the broker and change the business's launch state (9.787521ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (10.850372ms)
ℹ tests 7
ℹ suites 0
ℹ pass 5
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 8239.388311
✖ 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 (5036.954429ms)
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/q142r3/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 (2036.628037ms)
AssertionError [ERR_ASSERTION]: the late reply answers the first message, not this one
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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 ca328319 to coder\ncoder (RESULT, re 750d6421) d8a57d0b:\nlate answer\n',
expected: /late answer/,
operator: 'doesNotMatch',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142r3-out/mutants/.bak.iUhyT5/packages/cli/src/cli.mjs 2026-10-15 18:34:54.210904265 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:34:58.000851380 -0500
@@ -260,7 +260,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,61 @@
✖ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (47.608495ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1015.319432ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2021.038382ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (23.727957ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (9.166733ms)
✔ launches off and on go to the broker and change the business's launch state (9.0073ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (8.732653ms)
ℹ tests 7
ℹ suites 0
ℹ pass 5
ℹ fail 2
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3223.830681
✖ 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 (47.608495ms)
AssertionError [ERR_ASSERTION]: only the reply was marked read
3 !== 1
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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 (2021.038382ms)
AssertionError [ERR_ASSERTION]: nothing was marked read
+ actual - expected
+ [
+ {
+ args: {
+ id: 'ebf32a5d-903b-4e62-a456-da7e93e6deed'
+ },
+ business: 'demo',
+ verb: 'message.read'
+ }
+ ]
- []
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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/q142r3-out/mutants/.bak.iUhyT5/packages/cli/src/cli.mjs 2026-10-15 18:34:54.210904265 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:35:06.291948763 -0500
@@ -260,8 +260,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,36 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (45.119707ms)
✖ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1014.945942ms)
✔ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2018.026749ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (23.723111ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (9.286454ms)
✔ launches off and on go to the broker and change the business's launch state (8.963673ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (8.730428ms)
ℹ tests 7
ℹ suites 0
ℹ pass 6
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3216.686201
✖ 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 (1014.945942ms)
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/q142r3/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/q142r3-out/mutants/.bak.iUhyT5/packages/cli/src/cli.mjs 2026-10-15 18:34:54.210904265 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:35:09.567987244 -0500
@@ -266,7 +266,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,31 @@
✔ talk sends a REQUEST, prints and reads only the reply to it, and leaves other messages unread (43.011626ms)
✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1013.091508ms)
✖ a late reply the Console has listed stays unread for mosaic inbox, and the next talk skips it (2039.982715ms)
✔ talk says when the role isn't running, whether launch.by was told, and how to start launch.by (25.217476ms)
✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (9.708462ms)
✔ launches off and on go to the broker and change the business's launch state (9.690855ms)
✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (9.968801ms)
ℹ tests 7
ℹ suites 0
ℹ pass 6
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3241.852606
✖ 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 (2039.982715ms)
AssertionError [ERR_ASSERTION]: the late reply answers the first message, not this one
at TestContext.<anonymous> (file:///mnt/storage/scratch/researcher/q142r3/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 7f9e28a0 to coder\ncoder (RESULT, re 258132c0) 93e127f4:\nlate answer\n',
expected: /late answer/,
operator: 'doesNotMatch',
diff: 'simple'
}
@@ -0,0 +1,11 @@
--- /mnt/storage/scratch/researcher/q142r3-out/mutants/.bak.iUhyT5/packages/cli/src/cli.mjs 2026-10-15 18:34:54.210904265 -0500
+++ packages/cli/src/cli.mjs 2026-10-15 18:35:12.836025632 -0500
@@ -260,7 +260,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,54 @@
b4d1b01a60c22d2471f8e564d9ca70def0a52ce2129bb89852073cb52cf61f2a ./build.md
c847d8fe411d1269bd751c595048d289f4a7c1f76f639b6acfe622e6d593dd81 ./candidate-manifest.sha256
f2575be42ec9507bd02f1cf14e0f6437600bab330d61a709738b61d743d7f89b ./changed-paths.txt
b9172cfb5d91bb6d34a50169366b787d8e805eb5466d66b9ec8a8adfa4cd8ea4 ./cli-single-finished.txt
9b3d1c07d6342106520ea6488a5a2de8444898a49612d9056b670632f46fef5b ./cli-single-started.txt
552152338f299a6e5bee843c79aad847c5ec064a8d36bdc328029582fbba4200 ./cli-single/_summary.txt
a27966f12b4f860e95cdc8b51cb0cab2ecf60651aec2ef6b42d200368069a271 ./cli-single/app.out
3f64f6c20c4909a10c31211d7cd48b1d305c3b05360bc401b3b86d7e3c0b9ec3 ./cli-single/boot-status.out
833eab7f5ddc4108a8def0f0c7f374aa5e64c389266597557493a3697c8c45e0 ./cli-single/cli.out
79976a85b9db9b60e651f07caaadafd6bf24f521aee9cda72319cd749387a1da ./cli-single/config.out
d233e01fc01849ac5079a75cee6b36c01e0564144cf67d9e6dcf0932aa4196c0 ./cli-single/follow.out
a4440d8c011f5fcd2ce2128cdbdca14df55e5655c11168dd0eafb967eec8641e ./cli-single/format.out
9fda58c15c82dc41a1e7eec9900c8e847bf6c41a2f38a09fcc206a60508275f5 ./cli-single/git-socket.out
91e38133ee463e7db5cc06def8f68dd31f7a79063e1124213166d69cb9bd1362 ./cli-single/host.out
532b8a4d5d419d1b418e86b0d577c891d4076e33f0318bcefa47b131f776c1e4 ./cli-single/launch-limits.out
6c21dacb3527c7611e8304278835a1db4cd95ac24ca8aa581dc98a1fb57da21a ./cli-single/launch-notice.out
3489c60a6643808a9b4a00f475a948252aa478b986cae4a90392eb9a9da1b55e ./cli-single/launcher.out
46553d8ba62443458c95b04f501f1d85f1bfba866c4101484c3c060adc9573cd ./cli-single/limits.out
6e17f91d01a2643de0028e085d5e854479904a36f54ad40dffdd2d314a7a0626 ./cli-single/notifier.out
1f84d887bf0c9a9a4236d849cc850fe9cc4d8a513a5cef758f7ac8099b0f88b7 ./cli-single/other-tree.out
76eb902d8636b4d149e9df845e9a2a3d3fd343be7694ac47cd2c8f3f9bd9cf52 ./cli-single/stack-copy.out
da09fadef2054e5d6f79e2c7e543bcbb9d8a1632fbfa21176701eb98d5ecc38c ./cli-single/tester.out
ae5baa41900cbcc5b0ae180a845eee89b3f4fb15aec71ea6856d228aa58c4e77 ./cli-single/thread-launch.out
f3dff47c254b3b32867f7813268d97ac870f2abcb6179489641db78dfdd8c35c ./cli-single/trackers-boot.out
5d72d202c1f817d79ba8014c1a9f653a8a461f0141e42ccef6b3169e96926a01 ./cli-single/transport.out
50ae356b66535bd1b29bfc7fe31a7264da4c66dd5e6c3419ae3747b1cd0e0dbc ./cli-single/verbs.out
15b47f1246ff67079e25503d07d17741cd60992ab3b5ac0479736fd082c077f5 ./mutants-finished.txt
2d3b650a72bfa70fe9c27bb3afd79d11922d7b56d89beb3069db964f8f3d1106 ./mutants-run.txt
5ac21653379dd14f19de22243830b4ed0132c4489065ea022d6dda6f5883c779 ./mutants-started.txt
1ec7f3c758565d2289e0d2a78633a0073ae3ead79a32da658d6193216693f7a9 ./mutants/control-follow.out
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa ./mutants/control-follow.rc
adbfbb9f75e504081f36cdd90da8c664703d40e00d857cb6476148a30567c86c ./mutants/control-tester.out
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa ./mutants/control-tester.rc
a11021be29b08600754c4200b07a3d0c941907c912891c18b02bec35b27609ad ./mutants/control-verbs.out
9a271f2a916b0b6ee6cecb2426f0b3206ef074578be55d9bc94f6f3fe3ab86aa ./mutants/control-verbs.rc
b62597c4c91cc31886743be51a49fcae7196658d76c8114147b7146edcd6d064 ./mutants/m1-filter-removed.out
d3e20265ba70fb2abb0573f7b2d239eeb72d00ab63babd317a007a7f59bb9d5a ./mutants/m1-filter-removed.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 ./mutants/m1-filter-removed.rc
9355276b1a2f0ccd25bbe130e834a3da6e1fcf63db4234c35dddab7bb55fcec0 ./mutants/m2-filter-under-follow.out
586d9c5cfda73133fb3af8eacac531811ede87360f5e71e5039253df051c037c ./mutants/m2-filter-under-follow.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 ./mutants/m2-filter-under-follow.rc
3d5fb6c9e121bc435bc5fe2e8e9493c5c01f3b6f6c7d596da91d4949982c6802 ./mutants/m3-filter-inverted.out
3454b77027bd8b418304866ab1b255b709869feda15ef77d2114f4037fb4f805 ./mutants/m3-filter-inverted.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 ./mutants/m3-filter-inverted.rc
5c371d9931d0a85fc7c7e9d718fa7a20234e7b4c163bfc4f2013de9fa473d7a9 ./mutants/m4-read-all.out
5bb59544dae7da2dd0e6c96ec7981850968ec538616b5fbe76825518e3be3fab ./mutants/m4-read-all.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 ./mutants/m4-read-all.rc
32817658aa1e3fa40a5d1f4fb8bcad29d9d594e4fcf6c9b2bd8e7f31daf2c842 ./mutants/m5-timeout-message.out
c52708605fa2b809b4bcdc6ddb9dfee03dc751750475dd0593c0f3c2cc437dd5 ./mutants/m5-timeout-message.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 ./mutants/m5-timeout-message.rc
35a7b676c25ce870d10764184a4919732730a46d8a6cdb94bd116b0c81d0b5dd ./mutants/m6-any-reply.out
495703f27f2ac93a35211b2499597f35d0406b9ae4eebd4ec006bb94fd680daf ./mutants/m6-any-reply.patch
4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865 ./mutants/m6-any-reply.rc
aa549297fc6c239721907679fb434845d476bc613905e0a17c124f91fb586a9f ./row142.patch
@@ -0,0 +1,166 @@
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 6d9756872..2a32f158a 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -62,10 +62,11 @@ 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. When no run holds the role (row 146, #1640),
+ 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. When no run holds the role (row 146, #1640),
a line after `sent` says so: `pm isn't running, chief has been told`
when the bus told `launch.by`; `pm isn't running, and the business names
no launch.by` with no launch block; `pm isn't running; a tester's message
@@ -77,8 +78,8 @@ mosaic launches list [--json]
shows `pm: on`) and stops running sessions. A hand-started host takes
`mosaic bus stop`, then `mosaic bus start <business> --pm`. No verb
launches `launch.by` on a running host. `--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`
+ 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 8db1a8b3a..e341a0b0e 100644
--- a/packages/cli/src/cli.mjs
+++ b/packages/cli/src/cli.mjs
@@ -225,8 +225,9 @@ function waitingLines(w, business) {
}
// `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");
@@ -258,12 +259,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 cb4a295be..f29922e0e 100644
--- a/packages/cli/tests/verbs.test.mjs
+++ b/packages/cli/tests/verbs.test.mjs
@@ -28,32 +28,42 @@ function setup(t, deps = {}, businesses) {
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`));
});
// Row 146 (#1640): the lines after "sent" when the role has no running session.