Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
14 KiB
Row 150 (#1644): the human marks skipped messages read, and the Console says what talk prints
Brief: docs/plans/2026-10-15_talk-followups.md, section "The human marks
skipped messages read, and the Console says what talk prints" (blob
b1c00f1285). Worktree base: eff8417b6 for round 1, 63c82f1c4 for round 2
(see "Round 2" at the end). Rows 142 and 146 are in both bases.
The command
mosaic inbox --read <id> [<id>...] [--business <id>] [--tester]
mosaic inbox --read-all [--business <id>] [--tester]
Both start with the page walk mosaic inbox already does: messages with
limit: 200 and after, keeping the messages with read false. The ids are
resolved against that list.
- An argument shorter than 8 characters refuses with
too-short, asdecidedoes for a decision reference. An empty argument would otherwise match every message. - An argument resolves when exactly one unread message's id starts with it,
so the 8-character id
mosaic inboxprints works, and so does a full id. - Two or more matches refuse that argument with
ambiguousand name each match in full. Nothing is read for it. - No match and a shorter-than-full argument refuses with
no-unread-match. - No match and a full message id (36 characters) goes to
message.readas given. The broker decides: a message that isn't the human's refusesmessage-not-found, and one already read returns{read: true}again. This is how a full id frommosaic trailor the Console reaches the broker, and how a non-human id gets the broker's code instead of a local guess. --read-alltakes every message from the walk. It marks them oldest first.
Every argument is tried, whatever happened to the ones before it. stdout gets
one line per argument, marked <short id> or not marked <argument>: <code>,
and then marked N of M. --read-all prints only the refusals and the count,
since the walk can list hundreds. If any argument was refused, the command
exits 2 after printing everything, with N of M not marked on stderr.
Errors that aren't about one message stop the command at once with their usual
exit code: an auth refusal (3), outcome-unknown (1), a transport failure.
The per-message refusals are the codes busExit maps to 2.
--read and --read-all together refuse (usage, 4). Either with --json
refuses too; the JSON inbox prints only decisions today and a second JSON
shape for one flag isn't worth it. --read with no ids refuses (usage).
--tester with either flag refuses too: the tester's inbox lists the
human's messages, but the tester's message.read looks for a message to the
tester, so every id would refuse message-not-found.
Commands that aren't inbox don't accept --read or --read-all; each
refuses as an unknown option.
Undelivered messages
message.read needs a delivered row first, and refuses not-recipient
otherwise. A message the human has never received has none: one that arrived
while no talk or follow ran is unread and undelivered. Row 142's skipped
messages are delivered, so they read at once.
So on not-recipient the command calls message.receive until a batch holds
the message or a batch comes back empty, then tries message.read once more.
A second refusal is printed like any other. message.receive takes the oldest
100 undelivered messages per call, and --read-all works oldest first, so one
receive usually covers the next 99 reads.
The cost: every message that receive call delivers stops being printed by a
later mosaic follow or talk --follow, which print only undelivered
messages. That is already true of every message plain talk skips since row
142. The command never prints a message it receives, and one it receives but
doesn't mark stays unread in mosaic inbox, so nothing goes missing. A
message that arrives after --read-all's walk can be delivered by its receive
and left unread. It shows in the next mosaic inbox.
The alternative was to receive everything before reading anything. That
delivers messages a single --read has no reason to touch, so I didn't.
The Console text
The send section now says that a reply shows under the receipt and stays
unread in mosaic inbox until it is marked read, and names mosaic inbox --read. The failure text under a receipt said mosaic talk prints the
replies; after row 142 it prints only replies to its own message, so that line
now points to mosaic inbox too. Both are in the send section, and both are
asserted in send-browser.test.mjs. The comment above the send section in
bus.js, the replies test's comments, and the broker.mjs comment above the
replies verb say the same.
Docs
docs/TOOLS.md: theinbox --readusage line, and a sentence beside--followthat names both flags and points to the cli README.packages/cli/README.md: the usage line and aninbox --readentry under "Human commands" (resolution,too-short,ambiguous,no-unread-match, output, exit 2, the receive and what it costs, no--jsonor--tester). The Console sentence now says a reply to a Console send stays unread ininboxuntil it is marked read. The--testerexclusions nameinbox --readand--read-all.packages/bus/src/broker.mjs: the comment above therepliesverb only. No code changed.
Tests, against the brief's list
All in packages/cli/tests/mark-read.test.mjs except the Console text.
| Brief | Test |
|---|---|
| Marking one message | "inbox --read marks one message by the id inbox prints, delivered or not, and leaves the rest unread" (a delivered message by its 8-character id, then an undelivered one by its full id) |
| An ambiguous prefix refused | "inbox --read refuses an ambiguous or short prefix, names every match, reads nothing for them and marks the rest" (also too-short for a 4-character and an empty argument) |
| An id that isn't the human's refused, the rest still marked | "inbox --read refuses an id that is not the human's, prints its code, still marks the rest and exits 2" (message-not-found from the broker and no-unread-match locally, exit 2, "2 of 4 not marked") |
--read-all leaves mosaic inbox at 0 unread |
"inbox --read-all marks every message the inbox walk lists, delivered or not, and inbox then shows 0 unread" (230 messages, more than one page and more than one receive batch) |
| The new Console text | packages/webui/tests/send-browser.test.mjs, whose assertions now name mosaic inbox and mosaic inbox --read |
Also new: "inbox --read stops on an error that is not about one message"
(outcome-unknown exits 1 and prints nothing) and "inbox --read and
--read-all refuse bad combinations; other verbs refuse them" (exit 4, no
broker call).
The brief lists not-recipient as a refusal to print. The command meets
it by receiving first, so not-recipient prints only when the message is
still undelivered after the receive loop. No test reaches that, because the
real broker always delivers the message by then.
Candidate
r1/build.patch against BASE eff8417b6
(sha256 8d4f29a6ae75819662860cd561ede0658880400a96bf519bfc24ceacface0212).
r1/candidate-manifest.sha256 (sha256
17d118eb74ce887a1d0d37f044ed107de23c67072a9dc2203d704a01e35f9ec8) lists 7
files: 6 changed and packages/cli/tests/mark-read.test.mjs, new. I applied
the patch to a fresh worktree at BASE: sha256sum -c passed on every line
and git diff --cached --check was clean (7 files, +204 -23). Canonical
HEAD was e7158ab14, and git diff eff8417b6 e7158ab14 on the 7 paths is
empty.
The public/ diff, for Mos's decision 123 go
r1/public-diff.patch is the packages/webui/src/public/ part of the
candidate: bus.js only, the send-section text and the comment above it.
Its sha256 is
096d922c75503db5bb67db0fb2e4c3b8be79d6d1ed9f1831ad81861ae3bfdca6, also in
r1/public-diff.patch.sha256.
Mos gave the decision 123 go on this sha256, 096d922c75503db5bb67db0fb2e4c3b8be79d6d1ed9f1831ad81861ae3bfdca6
(sage-0560). It lands after Filbert approves (28787) and Sage's gate is
green. A public/ diff with any other sha256 goes back to Mos.
mosaic inbox --json printing decisions only (base cli.mjs:193, candidate
line 263) is outside this row: the candidate leaves that line as it was and
only refuses --json together with --read or --read-all.
Checks
The slot was mine from 19:22:57Z (sage-0554). This round was targeted
(decision 136): r1/gate.sh ran each touched or adjacent test file on its
own with TMPDIR ~/darkwing-scratch/r150/tmp, no Docker. Output is in
r1/gate-run.log, step logs in r1/gate/. It ran from 19:22:58 to
19:23:33 UTC and ended ROUND_PASSED.
| Test file | Pass | Fail |
|---|---|---|
| cli mark-read (new) | 6 | 0 |
| cli follow | 6 | 0 |
| cli cli | 12 | 0 |
| cli tester | 7 | 0 |
| cli app | 30 | 0 |
| cli format | 3 | 0 |
| webui send-browser | 2 | 0 |
0 skipped in each. Afterwards no node or chromium process of mine had a cwd
under ~/darkwing-scratch (sweep.sh found 0), the r150 TMPDIR was empty,
and sha256sum -c on the manifest passed in the worktree.
Not run: the cli, bus and webui suites as a whole, the scripts/test-*.sh and mutants. Sage's gate at the landing target runs the suites.
Round 2 (sage-0575): rebase onto 63c82f1c4
Filbert approved round 1 (28800). Row 139 then landed on
packages/bus/src/broker.mjs (63c82f1c4, +62 -15 on that file), so the r1
manifest hash for that path no longer matches the landing target. Under the
base-drift rule the candidate comes back for a round. None of the other six
paths changed between eff8417b6 and 63c82f1c4, or between 63c82f1c4 and
canonical HEAD 690224f4a.
What changed: only the base
r1/build.patch applied to a worktree at 63c82f1c4 with git apply --index --3way, every file cleanly. r2/build.patch is git diff --cached of that
worktree.
The interdiff of r1 against r2. With the index and @@ lines removed, the two
patches are byte-identical (diff prints nothing). Those lines differ only in
the broker.mjs file header, where the base blob moved and the hunk sits 47
lines lower:
< index 031634bb8..32c2397cb 100644
< @@ -1710,8 +1710,10 @@ export class Broker {
---
> index 61ba1e4bd..f7258d656 100644
> @@ -1757,8 +1757,10 @@ export class Broker {
The broker hunk is still the comment above the replies verb and nothing else:
- // lists still reaches the next `mosaic talk`. An id naming no message in the business
- // refuses message-not-found, as message.send does for in_reply_to; trail would return [].
+ // lists stays unread in `mosaic inbox` until the human marks it read (`mosaic inbox
+ // --read`, row 150); since row 142 `mosaic talk` prints only replies to its own message.
+ // An id naming no message in the business refuses message-not-found, as message.send does
+ // for in_reply_to; trail would return [].
The manifests differ on one line, broker.mjs: r1 753c9368..., r2
9096792e599707c07c1394781590d4b04e8ce9ed334b9f6e3fc39b2f30141af7. The other
six hashes are the same.
The public/ diff is unchanged
r2/public-diff.patch is git diff --cached -- packages/webui/src/public/ of
the r2 worktree. Its sha256 is
096d922c75503db5bb67db0fb2e4c3b8be79d6d1ed9f1831ad81861ae3bfdca6, and
cmp against r1/public-diff.patch finds no difference. Mos's decision 123 go
(sage-0560) still covers it.
Chosen shape: receive, then retry (Filbert's note 3)
The brief lists not-recipient as a refusal to print. The candidate receives
first and retries the read, and I'm recording that as the chosen shape. Without
it, --read-all can't reach 0 unread while any message was never delivered.
The cost: one message.receive call delivers up to 100 messages, so the other
received messages besides the one being marked are now delivered. They drop out
of a later mosaic follow or talk --follow, which print only undelivered
messages. They stay unread in mosaic inbox, so inbox still lists them. Plain
talk has left skipped messages in the same state since row 142, and the cli
README says so.
The retry has no test (Filbert's note 2)
No test reaches a second not-recipient from the retry after the receive loop.
Filbert couldn't find a way to reach it for a human message in the current
broker either. Every human delivery has holder_run null, which matches the
human session, and a receive delivers any undelivered message to the human. So no
human message reaches that branch. If one ever does, the command prints it with
its code like any other per-message refusal.
Candidate round 2
r2/build.patch against BASE 63c82f1c4 (sha256
19d383466c7a628dcdb28d9353dc8e6f138f041b40d0142d56256db348aef5ed).
r2/candidate-manifest.sha256 (sha256
7556dda0fe75a1f76dd7423824da9c508ac48e44310740316e5f2e5d1fa34df9) lists the
same 7 files. I applied r2/build.patch with git apply --index to a second
fresh worktree at 63c82f1c4: sha256sum -c passed on every line and git diff --cached --check was clean (7 files, +204 -23).
Checks round 2
Sage granted the slot (sage-0578). Before the start, a /proc check found no
node --test process with a cwd outside my scratch. r2/gate.sh ran each
file on its own on the r2 worktree at 63c82f1c4, with TMPDIR
~/darkwing-scratch/r150/tmp and no Docker, from 2026-10-16T19:55:19Z to
19:55:47Z, and ended ROUND_PASSED. Output is in r2/gate-run.log, step logs
in r2/gate/.
| Test file | Pass | Fail |
|---|---|---|
| cli mark-read | 6 | 0 |
| bus replies | 7 | 0 |
| bus messages | 4 | 0 |
| bus threads | 14 | 0 |
| bus hard-waits (row 139's test) | 12 | 0 |
| webui send-browser | 2 | 0 |
0 skipped in each. Afterwards sweep.sh found no node or chromium process of
mine under ~/darkwing-scratch, the r150 TMPDIR was empty, and sha256sum -c
on the r2 manifest passed in the worktree.
Not run this round, per sage-0575: cli follow, cli, tester, app and format,
which round 1 ran on eff8417b6 and whose sources didn't move; the cli, bus and
webui suites; the scripts/test-*.sh; mutants. Sage's full gate at landing
covers them.