Lands Sage's sage/row66-r5 at7ffb640ac, round 4 (r4d82c830779) rebased ontoa36523713after row 197, approved by Dewey (comment 30085) and Filbert (30086) on manifest 6cfd812da4ca (23 paths, rev 2905). The rebase had no conflict. The range-diff is = except one pi-session.test.mjs hunk header. Patch-id without agents/ is d67275ac4251 on7b9737953,a36523713and this commit's parent. Gate (mos-1186): unslop, harness, cli and every scripts/test-*.sh green on r4d, harness 149/0 again on r5. Landing sessions on a scratch root from the candidate tree: Claude Code r4ab24630d323 showed the note, the injected notice and counts checked 2, flagged 1, notices 1. Pi r1dd94171bede is launch only until #383 (Vikunja #454).
201 lines
14 KiB
Markdown
201 lines
14 KiB
Markdown
# Row 66: managed sessions run the unslop check on both harnesses
|
|
|
|
Sage, 2026-10-20. Brief: `docs/plans/2026-10-11_managed-session-config.md`, section "Managed
|
|
sessions run the unslop check on both harnesses", blob 21ae25a328d4. Issue #1559. Reviewers:
|
|
Dewey and Filbert. Built in the worktree `/mnt/storage/scratch/sage-b66` (branch `sage/row66`)
|
|
on base 5487ed39d.
|
|
|
|
Candidate: see the commit line in the review request. `candidate-manifest.sha256` in this
|
|
directory has the sha256 of each changed file at the candidate.
|
|
|
|
## What changed
|
|
|
|
**Wiring.** Both harness modules mount `tools/unslop-hook/lists.json`, `unslop-check.js` and
|
|
the harness's own entry (pi `extension.ts`, Claude Code `claude-hook.js`) read-only at their
|
|
own paths, and the bundle manifest lists the three in `sources` with their digests. Pi loads
|
|
`extension.ts` as the second entry of `MOSAIC_EXTENSIONS`, after the gate extension. Claude
|
|
Code's `settings.json` gains a Stop hook and a UserPromptSubmit hook, each
|
|
`timeout -k 2 10 '<node>' '<dir>claude-hook.js' stop|prompt; exit 0`, hook timeout 20, no
|
|
matcher. Neither can block a reply or a prompt.
|
|
|
|
**claude-hook.js** (new). `stop` checks each assistant message with text since the turn's
|
|
prompt and logs `checked` or `flagged`. `prompt` checks the last reply and, when it has tells,
|
|
prints the extension's notice, word for word, as `additionalContext`, once per reply (it reads
|
|
the log for an earlier `notice_injected` with the same entry id). A broken lists.json logs
|
|
`broken_lists`; any other fault logs `hook_failed`; the exit is always 0.
|
|
|
|
**The operator channel.** The runner sets `MOSAIC_UNSLOP_LOG` to `<run>/unslop.jsonl` after the
|
|
module's variables, so a module can't point it elsewhere. After the landing turn and after each message turn it reads what the
|
|
log gained (whole lines only, from an offset). The next RESULT it sends ends with a note. The
|
|
whole note, both parts and the blank line before them, is at most 600 characters (r2):
|
|
|
|
- `[unslop: N reply|replies of this session flagged since the last note: <details>. The session gets a correction notice on its next turn.]`
|
|
- `[unslop: the check is broken, so this session's replies go unchecked: <reason>]`, once a run.
|
|
|
|
That RESULT is what `mosaic talk` and `mosaic follow` print. A turn that sends no reply (an
|
|
incoming RESULT, the landing turn) keeps its note for the next one. `note()` doesn't change
|
|
state and `sent()` runs only after `message.send` succeeds, so a failed send keeps the note.
|
|
`clip()` caps the body at 32000 characters plus its 33-character cut line before the note, so the
|
|
sum is at most 32633, under the bus limit of 32768. The broken reason is cut at 300 where the
|
|
runner takes it, from the log or from its own read failure, so the broken part is at most 371
|
|
characters and the flagged part is cut to what is left.
|
|
|
|
**Counts.** `unslopCounts(path)` reads the log after the session ended:
|
|
`{checked, flagged, patterns: {<source>: n}, notices, broken}`. A missing log gives zeros.
|
|
Findings now carry `source`, the entry's pattern id (`ms-unslop:7`), so `patterns` is per id.
|
|
|
|
**The stack copy (r2).** `scripts/stack-copy.sh` `COPY_PATHS` gains `tools/unslop-hook`. Both
|
|
modules join their files under `REPO`, and a session launched from a pinned copy has
|
|
`REPO = copies/<sha>`. Without it every managed session from a copy past this row would fail at
|
|
bundle build with ENOENT (Dewey P1, comment 29440).
|
|
|
|
## Round 2
|
|
|
|
Round 1 (candidate 8ec03b45a, manifest e68c6bce5ec4) got changes from Filbert (29439) and Dewey
|
|
(29440). Round 2 carries:
|
|
|
|
- F1 (both): a note with both parts wasn't bounded. A 300-character reason, a long flagged part
|
|
and a clipped answer came to 32934 (Filbert) or 33004 (Dewey), over 32768, and the broker
|
|
refused the send. `note()` now gives the flagged part only the room the broken part leaves, and
|
|
`fresh()` cuts every reason at 300, the runner's own read-failure reason included. Tests: the
|
|
runner test with an answer of 40000 characters, a 400-character `hook_failed` reason and 30
|
|
flagged replies (note ≤ 600, body ≤ 32768, cut line at 32000), and a unit check that both
|
|
parts come to exactly 600 and that a read failure on a 250-character path is cut.
|
|
- F2 (Filbert; Dewey's M3): no test failed a send through `main()`. The new runner test has the
|
|
broker refuse the first RESULT. The note then rides the next RESULT. One correction to the
|
|
F1 consequence as written: the broker marks a received message delivered, so the refused
|
|
message isn't received again in that run. It stays unread and its sender gets no reply; the
|
|
turn doesn't rerun.
|
|
- P1 (Dewey): `COPY_PATHS` gains `tools/unslop-hook`. The copy fixtures in
|
|
`stack-copy.test.mjs` and `pinned-copy.test.mjs` copy `tools/unslop-hook` too, and the
|
|
pinned-copy top-level list gains `tools`. A new `stack-copy.test.mjs` test makes a copy, loads
|
|
both harness modules from it, and checks that the adapter, the six unslop paths and the mounts
|
|
are under the copy and exist.
|
|
- N1 (Filbert): the two session-test comments say `<dir>/unslop.jsonl`.
|
|
- N2 (Filbert), doc-only follow-up: the brief's `<run>/session/unslop.jsonl` (:463) and "print
|
|
mode notifies nobody" (:477) are stale. The brief is Jason's text for this row; the deviation
|
|
below records the path.
|
|
- Dewey's notes, no change: the Claude Code Stop timing (finding 3 below) gets a look in the
|
|
landing session; `patterns` keys per entry source, which is the brief's "per pattern".
|
|
|
|
r2 mutants, each killed by its intended test (`mutants-r2.log`):
|
|
|
|
| Mutant | Killed by |
|
|
|---|---|
|
|
| K1 the r1 `note()` cut | the clipped-answer runner test (the send is refused and it times out) and the unit check |
|
|
| K2 `fresh()` doesn't cut the reason | the unit check |
|
|
| K3 `sent()` before `message.send` | the refused-send runner test |
|
|
| K4 `COPY_PATHS` without `tools/unslop-hook` | the copy test |
|
|
|
|
## Ship list against the brief
|
|
|
|
| Brief item | Where |
|
|
|---|---|
|
|
| A reply with a tell gets a correction notice on the next turn, both harnesses | `pi-session.test.mjs`, `claude-session.test.mjs`, row 66 four-turn tests: notices per request `[0, 1, 1, 2]`, notice text matched in the second request |
|
|
| A clean reply gets none | same tests: turn 2 is clean and turn 3's request carries no new notice |
|
|
| Broken lists.json fails closed: no clean verdict, operator told once, session alive | the row 66 broken-lists tests per harness: checked 0, flagged 0, first note broken, second note empty, the session answers both turns |
|
|
| Counts readable after the session ends | `unslopCounts` on each session's log: `{checked: 4, flagged: 2, notices: 2, broken: null}`, patterns non-empty |
|
|
| Interface 1 and the export list unchanged | no harness module export added or removed; the runner gains `unslopLog`, `unslopCounts`, `unslopNotes` |
|
|
|
|
## Findings made while building
|
|
|
|
1. **extension.ts read the wrong reply.** Pi 0.85.1 `getBranch()` returns entries root first
|
|
(`session-manager.js:958-967` reverses the leaf-to-root walk). `before_agent_start` took the
|
|
first assistant entry it met, which was the session's first reply, every turn. In the
|
|
four-turn test the old code gave notices `[0, 1, 2]` over three turns (turn 3 re-noticed
|
|
turn 1); walking from the end gives `[0, 1, 1]`. Fixed in `extension.ts`; this also affects
|
|
the fleet copy under `~/.mosaic/tools`, which I didn't touch.
|
|
2. **claude-hook replies() merged replies.** Grouping by `message.id` across the transcript
|
|
merged two replies that shared an id (the test API answers `msg_mock` for all). It now groups
|
|
adjacent assistant lines only, keyed by the first line's uuid. Before the fix turn 4 got no
|
|
notice (`[0, 1, 1, 1]`).
|
|
3. **Claude Code Stop timing.** On the first turn of a new session the transcript didn't yet
|
|
hold the reply when Stop ran, so the hook checked `last_assistant_message` with an empty
|
|
`entryId`. On resumed turns the reply was in the transcript. The verdict is the same either
|
|
way; only the entry id differs.
|
|
4. The finding detail format is `banned word "delve" x1`.
|
|
|
|
## Deviations
|
|
|
|
- **The log path is `<run>/unslop.jsonl`, not the brief's `<run>/session/unslop.jsonl`.** A
|
|
thread run's session directory is the thread's store, and
|
|
`packages/cli/tests/thread-launch.test.mjs:179` pins that a thread run keeps no per-run
|
|
`session/`. My first build made `<run>/session` for the log and failed that test in the gate
|
|
(452/453). The brief's other line, "`unslop.jsonl` in the run directory", still holds, and each
|
|
run keeps its own log.
|
|
|
|
- **`packages/seat/tests/sandbox.test.mjs`** isn't an owned file. Its pin
|
|
`binds(args).length === list.length - 1` broke because the modules now mount three more files.
|
|
It is now `- 4`, and the test asserts the three unslop read-only binds by name.
|
|
- **`scripts/stack-copy.sh`** and the two copy tests aren't owned files. The copy change is P1
|
|
above. PB3 of the Part B window (`repin-partB-proof.md`) holds `scripts/stack-copy.sh` still
|
|
until B7, so this row lands after the Part B window has run.
|
|
- `MOSAIC_EXTENSIONS` is set through the pi module's `env`, not in the runner.
|
|
- `unslop-check.js` gained the `source` field on findings (needed for per-pattern counts).
|
|
|
|
## Limits
|
|
|
|
- The log is in the run directory, which the session can write. A session could forge or erase
|
|
its own counts. They are the session's report, not host evidence.
|
|
- The pi extension's dedupe is in memory: after a restart, or when a turn's final message is
|
|
tool-only, the same earlier reply can get a second notice.
|
|
- `hook_failed`, `skipped_broken` and `broken_lists` all count as broken for the operator note.
|
|
- No blocking (Q84 option B), no list changes, no skill changes: out of scope by the brief.
|
|
|
|
## Gate
|
|
|
|
Recorded with the candidate in the review request: test-unslop-check, the harness, plugins,
|
|
seat and cli suites, and every `scripts/test-*.sh`. At landing Sage runs one managed session per
|
|
harness and attaches the notice, the correction and the counts.
|
|
|
|
## Round 4
|
|
|
|
Round 3 (2369c00e3 on sage/row66-r3, base c40d846cb, manifest 42e3876ca168) had approvals from
|
|
Filbert (29770) and Dewey (29771). Since then rows 200, 199, 232, 233, 240 and 238 changed three
|
|
of the 23 paths: `docs/TOOLS.md`, `scripts/stack-copy.sh` and
|
|
`packages/cli/tests/stack-copy.test.mjs`. Round 4 is the two round 3 commits cherry-picked onto
|
|
4ced5e63d (sage/row66-r4). Neither pick had a conflict, and the tree equals a hand merge of each
|
|
path with `git merge-file`. No code changed: `git diff BASE TIP | git patch-id --stable` is
|
|
6d085621fd35 for both rounds.
|
|
|
|
- Row 232 added `copy --bundle` (`copy_bundle`). It builds the bundle with `copy_tree`, which
|
|
copies `COPY_PATHS`, so a bundle copy also carries `tools/unslop-hook`. No extra change needed.
|
|
- The Part B hold under Deviations is void. Part B was superseded by the Q175 SSO work and will
|
|
not run, so nothing holds `scripts/stack-copy.sh`.
|
|
- Review scope: the merge on the three moved paths and this section. The other 19 paths have the
|
|
same blobs as round 3.
|
|
|
|
Rebased again on fefd54e13, after row 212 landed (ee7b2f137), as branch `sage/row66-r4b`, before the round 4
|
|
request went out. 212 added `packages/library/src` to `COPY_PATHS` and a library line to the
|
|
`stack-copy.test.mjs` file list. None of the four picks had a conflict. Every file's patch-id equals the 4ced5e63d
|
|
build's except `scripts/stack-copy.sh`, where the only change is the hunk's context line
|
|
(`packages/queue/src packages/library/src`). Row 66's two added lines are the same.
|
|
|
|
Rebased again on 29ce1e360, after rows 195, 213 and 231 landed, as branch `sage/row66-r4c` (r4b stays at
|
|
49ad61963). Two of row 66's paths moved: `adapters/README.md` and `docs/TOOLS.md`. TOOLS.md merged without a
|
|
conflict. In adapters/README.md, row 213 rewrote the skill-cap paragraph, because `claude` now gets the prompt
|
|
as a file. The merge keeps 213's paragraph whole and adds row 66's "The unslop check (row 66)" section after
|
|
it. Row 66's old cap sentence is dropped, since it described the argument limit that 213 removed. Every other
|
|
path has the same blob as on r4b. Row 213 also changed the harness and both adapter scripts, so all of row 66's
|
|
harness tests run whole again on this base before the request.
|
|
|
|
Rebased again on 7b9737953, after rows 245, 248 and 249 landed, as branch `sage/row66-r4d` (r4c stays at
|
|
670b83f65). Only `docs/TOOLS.md` moved under row 66: row 245 rewrote the paragraph on what root reads in the
|
|
host check. The cherry-pick had no conflict. `git patch-id --stable` of row 66's whole diff is 508bb684eade on
|
|
both bases, and so is each file's own patch-id, TOOLS.md included. So row 66's hunks are byte-identical and only
|
|
their context moved. The one test that names TOOLS.md, `packages/cli/tests/stack-release.test.mjs`, uses it in a
|
|
message string and is in the singles list.
|
|
|
|
Rebased on a36523713 after row 197 landed (mos-1186 item 4), as branch `sage/row66-r5` (r4d stays at
|
|
82c830779). Row 197 also touched `packages/harness/tests/claude-session.test.mjs` and `pi-session.test.mjs`.
|
|
The cherry-pick had no conflict. The range-diff shows commit 1 differing only in one hunk header of
|
|
pi-session.test.mjs, where git now names the context line. Commits 2-7 are `=`. `git patch-id --stable` is
|
|
0049d05fb585 for the whole diff and d67275ac4251 without `agents/` on both bases. Correction: the r4d note
|
|
above gives 508bb684eade for the whole diff. That number came from e25f76f54 before the 7b9737953 rebase. The
|
|
whole-diff number on r4d is 0049d05fb585. The harness suite runs whole again on this base before landing.
|
|
|
|
Correction (Filbert round 4 N2, comment 30086): the r4c paragraph says "Row 66's old cap sentence is
|
|
dropped." That's wrong. In round 3's diff the 64 KiB cap sentence is unchanged context, so row 66 never
|
|
wrote it. Row 213 replaced it, and none of row 66's lines were dropped. The landing sessions are in
|
|
LANDING-SESSION.md.
|