# 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: dda7e31b0c0fbd3e85c143dafa864ef0a29883c9 (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) 1. **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@`, because the live host runs as that unit installed with --pm. The README gives the hand-started form (`mosaic bus stop`, then `mosaic bus start --pm`). **Refined while building:** `bus start --pm` is 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 `, which prints `pm: on` or `pm: off`. 2. **The notice's sender is from_role `_bus`, from_run `bus`.** ID_PATTERN is `/^[a-z0-9][a-z0-9._-]{0,63}$/`, so no role instance can be named `_bus`. Plain `bus` would pass requireId and could collide. 3. **The notice's class is RESULT.** See "Why RESULT" below; the reason as first stated was wrong in one detail, corrected there. 4. **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. 5. **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: 6. **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, with `told: false`, and talk says "a tester's message tells nobody to launch it". A message to the tester, like one to the human, gets no `waiting` field: no run ever holds either. 7. **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 in `this.#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_to` the 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. - `#waiting` returns null for the human, the tester, a thread instance in a thread, or a target a run holds (`#recipient`). Otherwise it reads `launch.by`, decides `told` (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}`. `launcherRunning` is null with no launcher. - `#notice` inserts the message directly (it skips requireId, which `_bus` fails) and routes it to launch.by through `#route`, so a stopped chief gets it at claim like any waiting message. Body: `Message from to waits: has no running session. Launch within the launch block's limits, or tell why not with message.send in_reply_to .` **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: ` isn't running[ either]. systemctl --user restart mosaic-bus@ starts it if the unit runs with --pm (scripts/bus-service.sh status 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 `_bus` names 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 message `in_reply_to` the waiting message. Don't reply to `_bus`. - sage-0407 item 3: "raise a `policy.change` decision until the broker gives you a verb for the organization file" is replaced. Row 132 gave that verb, the `change_organization` tool (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 `_bus` in each field a caller controls: `to` refuses unknown-role; `from_role` and `from_run` refuse invalid-request (unknown fields); `class` refuses invalid-class; `in_reply_to` and `corrects` refuse invalid-request; `decision` refuses decision-not-found. - chief replying to `_bus` with `in_reply_to` the 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 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 is `in_reply_to` it), 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`. 1. 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, `launcherRunning` is true; a role sender (coder) tells chief too. 2. A message to a running role: no notice, result keys exactly `id`, `request`. 3. 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. 4. Launched but not claimed counts as no holder (brief: accepted, documented in the README). 5. 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_to` it), or a thread reply to the assistant. 6. No launch block: `{launcher: null, told: false, launcherRunning: null}`, no notice. 7. `_bus` internal, above. - **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 `_bus` count stays 1; once pm is claimed, cto gets the told line only. helpers.mjs `broker()` 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 `_bus` count 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_default` network on the host was created at 17:42Z, before this gate started, so I left it in place.