Candidate manifest 616f953fe09bfc198645886bc4c7228a0fdeafa8c89b1729f2be8a5222689607 on dda7e31b0.
Owner's gate 23:11:36Z-23:15:46Z exit 0; mutants 17 of 17 killed.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
14 KiB
Row 146 (#1640): a message to a stopped role tells the launcher and the sender
Builder: Dewey. Reviewer: Filbert. Gate owner: Sage. Brief: docs/plans/2026-10-15_q167-rows-r2.md, lines 32-110 (the preamble of 2026-10-15_q167-north-star-rows.md applies). Calls agreed in sage-0404; priority and the chief-of-staff.md fix in sage-0407.
Base: dda7e31b0c (base.txt, origin/refactor after rows 144 and
145). The candidate is build.patch on that base, and candidate-manifest.sha256 lists every
changed path with its sha256. repro.sh applies the patch to a fresh detached worktree and checks
the digests.
The calls (sage-0404: agreed, written as stated)
-
No bus autostart of launch.by. talk prints a restart line instead. The launcher doesn't know whether the host was started with --pm, and a host without it means the operator chose no PM. An autostart spends tokens with no human command. A dedupe per waiting message would live only in host memory. It is also the smaller surface on the critical path. The autostart stays a follow-up. The line names the unit,
mosaic-bus@<business>, because the live host runs as that unit installed with --pm. The README gives the hand-started form (mosaic bus stop, thenmosaic bus start <business> --pm).Refined while building:
bus start --pmis the only path that launches launch.by (packages/cli/src/launcher.mjs:467-472, launchPm). A unit installed without --pm restarts with no PM, so the line now says the restart starts it "if the unit runs with --pm" and names the check,scripts/bus-service.sh status <business>, which printspm: onorpm: off. -
The notice's sender is from_role
_bus, from_runbus. ID_PATTERN is/^[a-z0-9][a-z0-9._-]{0,63}$/, so no role instance can be named_bus. Plainbuswould pass requireId and could collide. -
The notice's class is RESULT. See "Why RESULT" below; the reason as first stated was wrong in one detail, corrected there.
-
No notice for a message to launch.by, or in a thread to a thread instance (the brief's two), or from launch.by (added). launch.by is told through message.send's result, and otherwise chief's own auto-RESULT to an ended run would wake chief again.
-
The notice is sent only from message.send. review.request, thread.send and decision-close messages to a stopped role still wait silently. Follow-up.
Two more calls made while building:
- A tester's message tells nobody. A PM test (
--tester) authorizes nothing, so it wakes no launcher. message.send's result still says the role isn't running, withtold: false, and talk says "a tester's message tells nobody to launch it". A message to the tester, like one to the human, gets nowaitingfield: no run ever holds either. - Dedupe is structural, not a store lookup. The brief says "deduplicated through the
store". Only message.send calls
#notice, once, for the message it has just stored, inside the verb's transaction (every message verb runs inthis.#store.transaction). A re-route at claim (#routeWaiting), a release, a restart and a new claim never reach#notice, so a second notice for one message has no path. I wrote a store pre-check (SELECT ... WHERE from_role='_bus' AND in_reply_to=?) first and removed it: it could never find a row, so it was dead code no test could reach. The notice row itself is in the store,in_reply_tothe waiting message, and test 3 checks the count across claim, release, restart and a new claim.
The change
packages/bus/src/broker.mjs
NOTICE_ROLE = '_bus',NOTICE_RUN = 'bus'.- message.send ends with
#waiting(s, mid, a.to, thread). Its result is{id, request}as before when a run holds the target, and{id, request, waiting}when none does. #waitingreturns null for the human, the tester, a thread instance in a thread, or a target a run holds (#recipient). Otherwise it readslaunch.by, decidestold(a launcher exists, the target isn't it, the sender isn't it, the sender isn't the tester), sends the notice when told, and returns{role, launcher, told, launcherRunning}.launcherRunningis null with no launcher.#noticeinserts the message directly (it skips requireId, which_busfails) and routes it to launch.by through#route, so a stopped chief gets it at claim like any waiting message. Body:Message <id> from <sender> to <role> waits: <role> has no running session. Launch <role> within the launch block's limits, or tell <sender> why not with message.send in_reply_to <id>.
packages/cli/src/cli.mjs — waitingLines, printed once after sent:
| Case | Line |
|---|---|
| no launch.by | pm isn't running, and the business names no launch.by |
| target is launch.by | the restart line for it |
| tester sender | pm isn't running; a tester's message tells nobody to launch it |
| told, launcher running | pm isn't running, chief has been told |
| told, launcher stopped | the told line, then the restart line with "either" |
Restart line: <role> isn't running[ either]. systemctl --user restart mosaic-bus@<business> starts it if the unit runs with --pm (scripts/bus-service.sh status <business> shows pm: on). The restart stops running sessions. The business is read only when a restart line prints.
roles/chief-of-staff.md
- A duty after the Launch bullet: a message from
_busnames a waiting message, its sender and the role. Launch the role within the launch block's limits, or tell the sender why not with a messagein_reply_tothe waiting message. Don't reply to_bus. - sage-0407 item 3: "raise a
policy.changedecision until the broker gives you a verb for the organization file" is replaced. Row 132 gave that verb, thechange_organizationtool (packages/harness/src/tools.mjs:151-152), which changes limits, the model table and each instance's model. Hiring an instance or changing what one may do still takes a policy.change decision, and the sentence now says so.
READMEs. packages/bus/README.md: the message.send row and a "Launch notices" section. packages/cli/README.md: talk's lines, the restart command with its --pm condition, the hand-started form, and that no verb launches launch.by on a running host. docs/TOOLS.md has no talk output detail, so it is unchanged.
No schema step. messages.class has no CHECK constraint, and RESULT is an existing class.
_bus stays internal (sage-0404)
#notice is private and has one caller, #waiting, inside message.send. No verb takes a sender.
launch-notice test 7 checks:
- ID_PATTERN rejects
_bus, so no business file can declare it. - For the human and a role (coder), message.send with
_busin each field a caller controls:torefuses unknown-role;from_roleandfrom_runrefuse invalid-request (unknown fields);classrefuses invalid-class;in_reply_toandcorrectsrefuse invalid-request;decisionrefuses decision-not-found. - chief replying to
_buswithin_reply_tothe notice refuses unknown-role. chief's answer to the sender stores from_role chief, and the notice count stays 1.
Why RESULT, and where the test lives (sage-0404)
harness/src/runner.mjs:386-397: for a message whose class isn't RESULT the runner auto-replies
with message.send to m.from_role, then calls message.read in the same try. If the reply throws,
it logs runner: reply to <id> failed and skips the read.
Correction to call 3 as first stated. I said a non-RESULT notice would be "processed again: a
turn loop". That is wrong. message.receive returns only messages with no delivered or read
row, and the notice is delivered once it is received, so it is never received again. What a
non-RESULT class would do: the auto-reply to _bus refuses unknown-role, the runner logs the
failure and skips the read, and the notice stays unread for good. No loop, but an error per
notice and an unread message in chief's inbox. With RESULT, chief takes one turn, sends no
auto-reply, and reads it.
The test is at runner level, in packages/harness/tests/runner.test.mjs: "the bus's launch
notice is one turn with no reply, and is not taken again". The runner runs as chief (the fixture
sets launch.by: chief), the human sends to coder, and the test waits for the notice to be read.
pm then sends chief a request, and its reply arrives, so the runner has taken another pass. It
asserts: the turns dir is exactly 0001.stderr, 0002.stderr (one turn for the notice, one for
pm); the notice has exactly one delivered and one read row; chief sent nothing in reply to the
notice; stderr has no reply to ... failed. Mutant B10 (class INFO) is killed by this test.
Readers of _bus (sage-0404)
Each renders from_role as text with no role lookup, so a _bus sender renders without an error.
Lines checked on the candidate:
mosaic read/follow: packages/cli/src/cli.mjs:212 (formatMessage) and packages/cli/src/format.mjs:78.- The Console: packages/webui/src/public/bus.js:174-176 and 181 (trail and grouping; a non-human
sender becomes
actor: {role, run}), 267 (a message's replies, so the notice shows under the waiting message, since it isin_reply_toit), 321 (a role's waiting list). - The runner: packages/harness/src/runner.mjs:65 (the turn's header line), 75 (the tester check, a string compare).
No change was needed in any of them.
Tests
- packages/bus/tests/launch-notice.test.mjs, new, 7 tests. Fixture: chief, assistant (the
shipped messaging lists, as threads.test.mjs does), pm, coder;
launch.by: chief;bus.tester: true.- A message to a stopped role: exactly one notice, every field and the full body checked, its
delivery waits with no holder; chief receives it at claim; with chief running,
launcherRunningis true; a role sender (coder) tells chief too. - A message to a running role: no notice, result keys exactly
id,request. - A re-route at claim, a release, a broker restart and a new claim: still one notice, and the new run receives the waiting message.
- Launched but not claimed counts as no holder (brief: accepted, documented in the README).
- No notice to launch.by, from launch.by, from a tester, to the human, to the tester (a tester
ping, then coder's reply
in_reply_toit), or a thread reply to the assistant. - No launch block:
{launcher: null, told: false, launcherRunning: null}, no notice. _businternal, above.
- A message to a stopped role: exactly one notice, every field and the full body checked, its
delivery waits with no holder; chief receives it at claim; with chief running,
- packages/harness/tests/runner.test.mjs, one new test, above. setup takes
launchBy. - packages/cli/tests/verbs.test.mjs, "talk says when the role isn't running, whether
launch.by was told, and how to start launch.by": the no-launch-block line; nothing for a running
coder; with launch.by pm, cto gets the told line and pm's "either" restart line; pm gets its own
restart line and the
_buscount stays 1; once pm is claimed, cto gets the told line only. helpers.mjsbroker()takes a businesses argument for it. - packages/cli/tests/tester.test.mjs, "talk --tester to a stopped role says nobody is told to
launch it": the exact output and a
_buscount of 0.
The brief's list: one notice (1), none for a running role (2), none at re-route and restart (3), both waiting lines and the restart line for launch.by (verbs).
Row 142
Row 142 (Researcher) also changes talk in cli.mjs and verbs.test.mjs. It had not landed at
dda7e31b0. Whichever row lands second rebases on the other. My cli.mjs change is one function
before the talk comment and one line after the sent print.
Follow-ups (not in this row)
- Notices from review.request, thread.send and decision close, which still wait silently (call 5).
- The bus starting launch.by itself (call 1), through the launcher tick and launchPm.
- roles/chief-of-staff.md's Protocol section still says "You have no write tool" and lists only
the launch and message tools; row 132 added
change_organization. Researcher's row 140 edits the same file, so I left it for 140.
Receipts
- repro.sh into a fresh worktree: DIGESTS_OK.
- Every test file the candidate adds or changes (sage-0413), on the candidate at 23:05:29Z, all 0 fail and 0 skipped: bus launch-notice 7, cli tester 7, cli verbs 7, harness runner 24. cli/tests/helpers.mjs is not a test file; every cli test imports it and the gate runs them all.
- Other singles before that, 0 fail: bus messaging 13, threads 14, tester 11, broker 16; business chief-of-staff 10.
- Base check: origin/refactor is
dda7e31b0. No commit in dda7e31b0..HEAD touches a manifest path.
The slot was granted by Sage in sage-0417 at 23:06:21Z.
-
Mutants: run from 2026-10-15T23:06:30Z to 23:11:24Z. 17 of 17 killed, and the source was restored; DIGESTS_OK afterwards. Details are in MUTANTS.md.
-
Gate: gate.sh run from ~/dewey-scratch/r146, 2026-10-15T23:11:36Z to 23:15:46Z, exit 0. The output is in gate-run.log and the logs are under gate/. It ran the touched packages and every package that imports the bus, then every scripts/test-*.sh:
Step Result bus 259 pass, 0 fail business 106 pass, 0 fail cli 209 pass, 0 fail harness 101 pass, 0 fail plugins 28 pass, 0 fail seat 135 pass, 0 fail tasks 97 pass, 0 fail webui 113 pass, 0 fail test-auth 15 passed, 0 failed test-conductor 17 passed, 0 failed test-config 24 passed, 0 failed test-discord 78 passed, 0 failed test-extension-package 18 passed, 0 failed test-foundation 44 passed, 0 failed test-land-work 84 passed, 0 failed test-queue 28 passed, 0 failed test-release 14 passed, 0 failed test-task 98 passed, 0 failed Flakes #258, #261, #262 and #263 did not recur. The packages that don't import the bus (control-board, conversation, discord, ledger, mosaic, runs) are left to Sage's full gate. business runs here because its chief-of-staff test reads roles/chief-of-staff.md.
-
Docker: the gate left no network. The only
wt_defaultnetwork on the host was created at 17:42Z, before this gate started, so I left it in place.