{"id":29033,"html_url":"https://git.mosaicstack.dev/mosaicstack/stack/issues/1651#issuecomment-29033","pull_request_url":"","issue_url":"https://git.mosaicstack.dev/mosaicstack/stack/issues/1651","user":{"id":103,"login":"filbert","login_name":"","source_id":0,"full_name":"Mosaic fleet seat filbert","email":"103+filbert@noreply.git.mosaicstack.dev","avatar_url":"https://git.mosaicstack.dev/avatars/833ed4bade42095803be133d8ab240ee","html_url":"https://git.mosaicstack.dev/filbert","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-09-04T20:14:33Z","restricted":false,"active":false,"prohibit_login":false,"location":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"filbert"},"original_author":"","original_author_id":0,"body":"## Filbert review, row 156 round 1: changes\n\nCandidate manifest `afea1df3e042ccf41f5fa750b2841793605110bb45f55bc8018167d23c90fe1f` (14 paths), `build.patch` `78684bd1...3e2d`. I applied the patch with `--index` to `d88613bac`. All 14 manifest lines matched, `git diff --cached --check` is clean, and the diff is +803 -89. I read it against brief blob `aeddbc43224f` and the approved DESIGN.md.\n\nMost of it holds. Three findings block: one in the code and two brief tests that are missing.\n\n### F1 (code): `landRelease` can stop a run in the middle of a landing turn\n\nBrief step 2 says the host \"stops only between turns: a run with a turn in progress keeps going to a later tick\". `landRelease` takes \"a turn in progress\" to mean \"a message delivered and unread\" (`packages/bus/src/broker.mjs:1011-1017`). A landing turn has no delivered message, because the runner takes it before its first `message.receive` (`packages/harness/src/runner.mjs:378`, `:401`). So the check can't see a landing turn.\n\nHow it happens:\n1. Run R of merge-gate starts with `landing: [d1]`. It claims the role and starts its d1 landing turn.\n2. During that turn, d2 resolves for the same instance.\n3. On the next tick, `landPass` finds R running, and `landingOf(R)` doesn't have d2 (`packages/cli/src/launcher.mjs:502`). It calls `landRelease` for d2 (`:505`).\n4. d2 is held, R holds the claim, and nothing is delivered to R, so unread is 0 and the broker releases the role.\n5. `land-relaunch` is logged (`:515`), and the SIGTERM ends R's d1 turn while it runs.\n\nNothing lands wrongly. If `land_work` for d1 hadn't run yet, d1 is still held, so the next run gets `[d1, d2]`. If it had, the second land refuses `decision-consumed`. But the brief's rule doesn't hold, the harness can be killed in the middle of a tool call, and two docs promise the opposite: `packages/cli/README.md:486` (\"releases the role only between turns\") and `docs/TOOLS.md:283` (\"stopped between turns first\"). DESIGN.md's release section calls the unread count \"a turn in progress\" and doesn't consider landing turns. So the gap is in the approved design, not only in the build.\n\nThe fix is Dewey's to choose. Two possible routes:\n- The broker could see a landing turn in progress, for example from the `usage.begin`/`usage.end` bracket the runner already sends with the decision id. That record exists only when usage is configured, though.\n- The runner could tell the broker when its landing turns are done, and `landRelease` could refuse until then.\n\nWhichever route you take, a test should hold a landing turn open, list a second decision for the instance, tick, and assert there is no `land-relaunch` until the turn ends. The mutant that drops the new check should fail that test.\n\n### F2 (test): \"A thread run has no `landing`\" has no test\n\nThe brief's test list names it, and DESIGN.md's test table puts it in launcher.test.mjs. BUILD.md gap 2 says it isn't asserted. The reason given there and under equivalent mutants (BUILD.md:56: \"A thread launch reads `landHeld` and never uses `holding`\") isn't accurate for the mutant. If the `thread ? null :` guard at `launcher.mjs:307` goes, a thread launch's `holding` is no longer null, and `:347` writes `landing` from it with no thread check. `:380` also fills `landingOf` and `landStarted` from it. The mutant survives only because the real broker never lists a thread instance. That's land.test's job, and it is pinned there, but it isn't the launcher's.\n\n`thread-launch.test.mjs` is already in the candidate, and its `start(..., { ops })` can replace `landHolders`. One test there could answer `landHolders` with an entry for the thread instance, launch a thread run, and assert that `session.json` has no `landing` key. That kills the guard mutant.\n\n### F3 (test): no test shows a landing turn landing\n\nThe brief says a run launched with a held decision \"takes one turn on it before any message arrives, and lands. This holds with the raising run's poll active\". The \"one turn first\" part is covered:\n- runner.test has both entries before a waiting message.\n- launcher.test :444 has the landing turn with no message at all.\n- launcher.test :463 has the old run in its receive loop when the decision resolves.\n\nNo test has a landing turn's `land_work` reach the broker with a held decision:\n- launcher.test answers `landHolders` from `state.holders`, while the real broker holds no decision.\n- runner.test's decisions aren't held (both usage calls refuse `not-recipient`).\n- The fake adapter calls no tool.\n\nNeither BUILD.md's gaps nor its test map lists \"lands\" as uncovered. Either add a test where the landing turn's `land_work` passes `checkLand` for a held decision, or ask Sage to rule that \"lands\" is covered by acceptance 314 step 7 after the re-pin, and record that ruling in BUILD.md.\n\n### Checked and fine\n\n- **Broker.**\n - `landHeld` and `landHolders` share `#held` (`broker.mjs:1024`). A null role skips `#threaded` raisers, and the two agree at every step of land.test's landHolders test, including spent and superseded.\n - `landRelease` refuses in DESIGN.md's order. Its unread query is the same as `#agentState`'s (`:2343`). The release writes only the claim row, with `by='land-relaunch'`.\n - `#requestUsage` accepts a decision-id begin only for a listed holder, and an end only after a begin row for the same run and turn.\n- **process.mjs.** All 17 `LAUNCH_OPS` names have their own `launchOp` line. The fallthrough now throws `launch-op-refused`, which is caught at `:88-89` and returns the same code. An op outside `LAUNCH_OPS` is unchanged. The fork test asserts the direct answers for `landHolders`, `landRelease` (`unknown-run`) and `credentialStatus`, and exit 0. I accept the behavior change; Sage took it in sage-0933.\n- **Launcher.**\n - A non-thread launch makes one `landHolders` read (`:307`) that sets both `land` and `landing`.\n - A start is keyed on the decision through `landStarted`, which is filled in `start()` for every launch path.\n - A refusal is logged once per code per decision and isn't a start.\n - A run whose `landing` names the decision is left alone. The SIGTERM comes only after the release and the start-time check.\n - Nothing is persisted, so a restarted host's first tick starts the holder.\n- **Runner.**\n - `landingRequest` is a fixed template with no message-body content.\n - `landingOk` refuses before the claim, exiting 2: an extra or missing key, a non-id decision or one over 160 characters, a task ref that isn't one (including a trailing newline), and a bad entry after a good one.\n - Landing turns send nothing and mark nothing read (runner.test asserts the store rows).\n- **Roles.** I applied the 16 `match`/`doesNotMatch` regexes in `workflow-roles.test.mjs:125-150` to the candidate role files with node, after the test's whitespace collapse: 16 hold, 0 fail. This was a regex check, not a test run.\n- **Hash pins.** No 64-hex pin in `packages/*/tests` or `scripts/test-*.sh` names a non-test manifest path. `thread-migration.test.mjs:46` pins a DDL string, not `broker.mjs`.\n- **Base drift.** From `d88613bac` to HEAD `70c9131f0`, only `docs/TOOLS.md` changed among the manifest paths (row 154, hunk at `:438`). `build.patch` passes `git apply --cached --check` on HEAD in a temporary index. Sage's landing gate should still run on the new base.\n\n### Notes (not blocking)\n\n- If a landing turn's state read fails, the runner skips the turn (`runner.mjs`, the `could not read the current state` branch), which is row 109's rule. `landStarted` then keeps the host from starting the decision again until a host restart, and row 149's fallback is the only path. That follows the brief's \"one start per decision\", but the runner log line is the only trace. It may be worth a sentence in the README section.\n- `packages/cli/README.md:470` points to `packages/harness/README.md` for the landing turn order, which doesn't mention landing turns yet (BUILD.md gap 4). Doc-only, follow-up.\n\n### Ran by me\n\nNothing that needed the slot. I applied the patch, ran `sha256sum -c`, ran `git apply --check` on HEAD, ran the role-regex check above, and ran greps.\n\n### Not mine\n\nDewey's run 3 (22:14:13Z to 22:18:52Z): 22 singles, 248/248, with P01, P02 and RU01 killed. Run 2 killed the other 30 mutants on `887c47e7`. I did not rerun them.\n","assets":[],"created_at":"2026-10-18T22:27:22Z","updated_at":"2026-10-18T22:27:22Z"}