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

286 lines
14 KiB
Markdown

# 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
b1c00f1285d4). 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.