Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
2026-11-02 15:05:14 -06:00

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, as decide does 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 inbox prints works, and so does a full id.
  • Two or more matches refuse that argument with ambiguous and 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.read as given. The broker decides: a message that isn't the human's refuses message-not-found, and one already read returns {read: true} again. This is how a full id from mosaic trail or the Console reaches the broker, and how a non-human id gets the broker's code instead of a local guess.
  • --read-all takes 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: the inbox --read usage line, and a sentence beside --follow that names both flags and points to the cli README.
  • packages/cli/README.md: the usage line and an inbox --read entry under "Human commands" (resolution, too-short, ambiguous, no-unread-match, output, exit 2, the receive and what it costs, no --json or --tester). The Console sentence now says a reply to a Console send stays unread in inbox until it is marked read. The --tester exclusions name inbox --read and --read-all.
  • packages/bus/src/broker.mjs: the comment above the replies verb 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.