9.0 KiB
Row 76, Console sends a message to a role, round 1 review (Darkwing)
Issue #1570, request comment 27594, packet agents/dewey/work/queue-76/
(commit 1deb6143). Brief: docs/plans/2026-10-11_dogfood-cutover.md,
"Console sends a message to a role". Base 3d298920. Candidate manifest
sha256 ca8703145aed98142c1fdf0553ab846a9b9a808cd58c35f7cbb4fa35d7f279a2,
9 files, matching the request. Sage asked for this round on content now;
the row re-rounds after row 59's landing, which changes the README and
bus.js digests.
Verdict: approve. The route refuses everything the brief lists before it calls the bus, and logs one line per send without the body. Dewey's decision 5, that a coded bus answer means nothing was written, holds on every path I traced from the socket to the page. The page sends once per press and never resends. Nothing blocks. Five notes and my answers to the two questions are below.
Method
- A detached worktree at
3d298920withgit apply --index build.patch, thensha256sum -con the manifest: 9 OK, and again after the mutants. None of the 9 paths changed from3d298920to1ab6e181at the start. - I read the whole patch, and the bus code the send passes through:
Broker.requestand themessage.sendcase,Store.transaction, the socket server,Client.call,human-cli.mjsandhumanCall. - Six WebUI test files, then 16 mutants of my own (
r1/mutants.sh) and a rerun of the four survivors against other files (r1/m2.sh). Each mutant edits one site, runs one test file and restores from the worktree index, usingagents/darkwing/work/queue-59-review/r1/mut.mjs.
Node v26.8.1, TMPDIR=~/darkwing-scratch/rv76/tmp.
While I was reviewing, Dewey's working copy of the manifest and
build.patch changed (uncommitted, the row 59 re-round). Everything here
is against the committed ca870314, which still checks 9 OK on my tree.
Is a coded answer really "not written"?
This is what the page's "Not sent" rests on, so I traced each way an error
code can reach sendOutcome (packages/webui/src/bus.mjs:78).
- The broker runs
#dispatchand the result's secret check inside oneBEGIN IMMEDIATEtransaction (broker.mjsrequest,store.mjstransaction). Anything thrown there, a failedCOMMITincluded, rolls back.#refusedthen writes itsaction.refusedevent in a separate transaction, or answersstorage-unavailableif it can't. Both come after the rollback. - The socket server (
server.mjs) callsbroker.requestsynchronously formessage.send, so itsrequest-timeout(line 27) can only fire before the request is handled.invalid-envelope,incomplete-requestandrequest-too-largealso come before the broker. Client.callturns every socket error, end or timeout intooutcome-unknown, and an answer it can't parse intoinvalid-response. It refuses an oversized request before connecting.human-cli.mjswritesinvalid-requestfor an uncoded error (line 37). Inside itstry, the only steps that can throw without a code run beforeclient.call. A failure writing the result to stdout surfaces as anerrorevent, not in thatcatch, and the parent reads that asinvalid-response.humanCallmaps a kill, a timeout and a missing code tooutcome-unknownorinvalid-response.
So the three codes in OPEN are the only ones a committed write can come
back with, and the server shows them as 503 sent: "unknown".
I also checked /api/config, the route order and the log line. The route
sits after the shared Host, Origin and sign-in checks and rechecks the
header itself, so a loopback request gets 401 with no log line. to is
JSON-quoted and cut to 64 characters before it's logged.
Row 59 has landed (a8512007). Its agents read still returns only held
claims, now with state fields, and each row keeps role, so the picker
works the same on top of it.
Tests and mutants
On the candidate, 01:16:27Z to 01:16:44Z on 2026-10-12
(r1/tests-light.txt):
| File | Result |
|---|---|
send.test.mjs |
13 pass, 0 fail |
send-browser.test.mjs |
1 pass, 0 fail |
shell.test.mjs |
5 pass, 0 fail |
preview.test.mjs |
11 pass, 0 fail |
bus.test.mjs |
9 pass, 0 fail |
serve.test.mjs |
4 pass, 0 fail |
I didn't run the full gate. Filbert's and Dewey's runs had the machine, and Dewey's gate is green with Docker.
Dewey's 21 mutants cover the request checks. I aimed mine at the sites
they don't name (r1/mutants.txt, 01:17:37Z to 01:18:51Z; r1/m2.txt,
01:19:14Z to 01:22:00Z):
| Mutant | Fails |
|---|---|
cli-no-bus-ok: --send starts without the bus config |
the CLI refusal test |
start-send-nonfunction: startServer takes a non-function send |
the startServer test |
open-no-invalid-response |
the outcome test, the transport test |
open-no-too-large |
the outcome test |
nonbus-definite: an uncoded error counts as definite |
the outcome test |
missing-to-passes: no to check |
the request checks test |
route-any-method: no 405 |
the request checks test |
status-mnf-502: message-not-found off the 400 list |
the outcome test, the real broker test |
success-unlogged: no id= line |
the send test, the transport test |
page-never-clears: the text is kept after a send |
the browser test |
page-sentfalse-ignored: a 5xx sent: false shows unknown |
the browser test |
page-no-config-gate: any config shows the box |
the browser test, on the rerun |
page-composer-always: no canSend check |
the browser test, on the rerun |
page-caret-lost: no caret restore |
survives send-browser and bus-browser |
app-reply-4xx-unknown: Reply never says "Not sent." |
survives 6 files |
app-reply-5xx-refused: Reply says "Not sent." for a 5xx |
survives 3 files |
My first run pointed the two config mutants at bus-browser.test.mjs,
which never turns send on. That was my mistake: send-browser kills
both. The last three are notes 1 and 2.
Notes
1. The Reply change in app.js has no test. Line 378 now says "Not
sent." for a 4xx. No WebUI test asserts the Reply error text, so making it
always or never "Not sent." passes browser, browser-edge,
return-flow, history-return-flow, preview and shell. The change
looks right to me (the board's replyToRow returns its 4xx before it runs
agent-send, and the Console's proxy errors are 502), but nothing holds
it. A browser case that answers /api/reply with 403 and then 502 would
cover it.
2. The caret restore isn't tested. build.md says focus and the caret
survive the refresh. Focus is checked; the caret line (bus.js:594) can
go and send-browser still passes. browser.test.mjs:55 does this for
the board's Reply box and could be copied.
3. "Recorded exactly like one sent from the CLI" isn't quite right
(README line 114). mosaic talk sends class REQUEST, and the Console
sends the broker's default, INFO, as the brief asks. The runner handles
both the same way, with a turn and a RESULT reply, but the role's model
reads class INFO in the message head. I'd say "the same way as one sent
from the CLI, with class INFO where mosaic talk uses REQUEST". Sage may
want REQUEST here later; that's a brief change, not this row.
4. The talk sentence (README line 185). The broker records each
message delivered when message.receive returns it; talk then marks it
read. "talk prints each one and records it delivered" credits talk with
the broker's step. The effect it states is right.
5. The refusal receipt repeats the code: "Not sent to coder: not sent: the bus refused it (unknown-role) (unknown-role)." The server's message already names the code, and the page adds it again. Cosmetic.
Notes 1 to 5 can go into the row 59 re-round or a follow-up, as Sage prefers.
Questions
1. Is it safe to show a proxy 4xx as "not sent"? Yes, under the
README's proxy contract, with one line added to it. Every 4xx the Console
returns on this route comes before send is called. An edge proxy's own
4xx (sign-in, 413, 400, 431, 429) comes before it forwards. A proxy that
has forwarded and lost the upstream answer gives 502 or 504, which the
page shows as unknown. The case that breaks it is a proxy set to replace
upstream errors with its own page, nginx's proxy_intercept_errors with
an error_page redirect for example. That could turn the Console's 503
outcome-unknown into a 4xx or a redirect, and the page would say "not
sent" about a message that may exist. I'd add to the contract: the proxy
passes the Console's status and body through and doesn't rewrite upstream
errors.
2. Should --send refuse to start without the bus config? Yes. The
operator asked for sending. A Console that starts and shows a box that
can only fail hides the problem until someone types a message, and
refusing at start is the repository's fail-closed rule. A read-only
Console still starts without the config, as before.
Round 2
When Dewey re-rounds on row 59's landing, I'll check the new digest, the
README and bus.js merges against row 59's lines, and rerun
send-browser, bus-browser and my mutants on the new tree.