Files
stack/agents/darkwing/work/queue-47-review/review-r1.md
T

7.8 KiB

Row 47, S5 follow-up (Mr flows test and the shim leak), round 1 review (Darkwing)

Issue #1533, request comment 27036, notes comment 27037, queue revs 277 and 278 (10351f6a). Brief: docs/plans/2026-10-10_s5-follow-up-and-hygiene.md (blob 24ceb88c), section "S5 follow-up". Base 21e0f1a3. Candidate manifest sha256 0587e393746e5cc6bb31e06b94461661fc7679f6f5fa6a72835c60b286a6055f, 6 files: five test files in packages/conversation/tests/ and Dewey's evidence.md. No src/ change.

Verdict: approve, comment 27043. The new flows test is the shape of my row 40 note 1 probe in both chunkings, and it's the only test Mr fails. The leak diagnosis checks out against the code: the shim ignores SIGTERM, nothing in src/ sends release, and two claim tests left a force-stopped scope for an after hook whose kill results nobody checked. Each test now reaps its own scope, every scope-launching file ends with a check that names a leak, and the after hook fails the file if a shim survives the sweep. Every leak mutant I tried in claim, cohort and races is killed, and afterleak reproduces the round 2 leak with the hook catching it. Two notes below, neither blocking.

Method

  • A detached worktree at 21e0f1a3 with the six files copied from a snapshot of the canonical tree, then sha256sum -c: 6 OK. Checked again after the mutants: 6 OK (r1/mut/manifest-after.txt).
  • I read the diff in full, src/shim.mjs, ScopeLauncher and forceStopCohort in src/cohort.mjs, #run in src/terminal.mjs, and W5, W13, K19, H22 and H23.
  • 10 mutants (r1/mut/mutate.py, run.sh), each run on the whole conversation suite with its own TMPDIR. After each run the runner lists any shim left under that TMPDIR, SIGKILLs its mosaic-chat-* scope and the PID, and restores the files from backup copies.
  • No shim was running before the gate, after it, or after any mutant other than afterleak (killed by the runner, see below). After all runs systemctl --user list-units --all 'mosaic-chat-*' lists nothing.

Node is v26.8.1, TMPDIR=~/darkwing-scratch/r47a/tmp. gate.sh set DOCKER_HOST=unix:///nonexistent.sock.

Suites

Suite Result
conversation 165/0
webui 22/0
test-auth 15/0
test-conductor 17/0
test-config 24/0
test-discord 66/0
test-extension-package 18/0
test-foundation 44/0
test-queue 27/0
test-release 4/0 without Docker, 14/0 with it (test-release-docker.txt)
test-task 26/2

The two test-task failures are "user recall run succeeds" and "recalled user name", the live worker check that needs Docker and a model call. They fail the same way in my row 41 gates. Dewey's run reports 98/0, so it ran the Docker cases; I didn't.

Mr

The test types Ctrl-T, Ctrl-O inside the text held behind it, hi Enter, then yo Enter and Ctrl-C, once as three chunks and once as one. It asserts sent is ["hi"], then held is null, then yo goes through and Ctrl-C quits. That's my probe/stacked-later.mjs from row 40, plus the single-chunk case, where the hold and the stacked hold come from one #parse. Under Mr (holds = false; at terminal.mjs:158) the suite is 164/1 and the one failure is this test (r1/mut/Mr-conversation.txt).

The leak

Dewey's reading matches the code:

  • shim.mjs ignores SIGTERM and SIGHUP and exits only on release once the engine cgroup is empty.
  • The only shim ops src/ sends are hello, term, events, freeze, members and kill (cohort.mjs:115-192). So a force-stopped scope stays up with its shim until something SIGKILLs the unit.
  • W5's probe and W13 force-stop a scope and left it to the after hook, whose systemctl --user kill and stop results were never checked.

The evidence names f55, the W5 probe fixture, as Filbert's leaked shim. My afterleak run leaked the same fixture (f55/sock/shim-cede1ee324e5.sock). Dewey says what the journal can't show, which is why the after kill missed in Filbert's run. The fix doesn't depend on knowing: the tests reap right away, and the sweep kills by PID whatever systemctl does.

The new helpers read /proc/<pid>/cmdline for node <...>/shim.mjs --socket <path under tmp>, which is exactly ScopeLauncher's argv (cohort.mjs:105). tmp is per test process, so each file sees only its own shims, and a parallel gate from another seat isn't touched. noprobe failing proves the detector finds a real shim.

K19's finally used to assert on kill before sending release, so a failed kill left the scope. It now sends release only after a good kill, SIGKILLs the scope if either fails, and asserts afterwards.

Mutants

Mutant Change Conversation Result
Mr holds = false; after re-parsing held text 164/1 killed by the new flows test
noprobe W5 doesn't reap its probe 164/1 killed: claim's "no shim ... left running"
now13 W13 doesn't reap its fixture 164/1 killed, same test
afterleak noprobe, and after doesn't reap or sweep 164/2 killed: the same test and claim's after hook; 1 shim left, killed by the runner
sweeponly noprobe, and after sweeps but doesn't reap 164/1 killed by the last test; the sweep alone cleans up, no shim left
sweeppid sweeponly, and killShims skips the scope kill 164/1 same; the PID kill alone is enough
k19leak K19's finally sends no kill and no release 164/1 killed: cohort's "no shim ... left running"
h22noreap H22 (force stop, then relaunch in a scope) doesn't reap 164/1 killed: races' "no shim ... left running"
h23noreap H23 doesn't reap 165/0 equivalent: H23 runs on pgroup, no shim
reapnosweep reap() without its killShims(fx.base) 165/0 survives, expected: it's a backstop for a missed systemctl

afterleak is the round 2 leak on purpose. Its hook failure names the shim (r1/mut/afterleak-conversation.txt:205), and node:test marks the file failed:

AssertionError [ERR_ASSERTION]: no shim outlives this file (#1533)
+   pid: 3459119,
+   socket: '.../tmp/mut-afterleak/chat03-lQNQDH/f55/sock/shim-cede1ee324e5.sock',
+   unit: 'mosaic-chat-cede1ee324e5b263ef2671152'

So all three scope-launching files have their final check killed by a leak mutant, which Dewey's table showed only for claim.

Notes (not blocking)

  1. liveShims takes the first cgroup segment ending in .scope as the shim's unit, and killShims SIGKILLs that whole unit. Today every shim comes from ScopeLauncher, so that's the mosaic-chat-* scope. A future test that spawned shim.mjs directly from a shell inside a terminal's app-*.scope would get that terminal's scope killed. Matching /^mosaic-chat-.*\.scope$/, which is what my runner does, costs one regex. Worth folding into the next change to harness.mjs.
  2. The brief asks for the Mr output in the packet. evidence.md has the table and the afterleak excerpt; the outputs stay in ~/dewey-scratch. Mine are in r1/mut/ if Sage wants them on file.

Finding for Sage

I agree with Dewey's out-of-scope finding and the recommended follow-up. In a running controller every force stop leaves a mosaic-chat-*.scope with an idle node shim, until the unit is killed by hand or the user manager restarts. The fix is release after a proven force stop (and on close of a stopped binding), with a test that the unit is gone. That's cohort.mjs or controller.mjs work, outside this brief.

Files

  • r1/candidate-manifest.sha256, r1/files.txt: copies of Dewey's.
  • r1/gate.sh, r1/out/: suite runs, summary.txt with a shim count after each node suite and at the end.
  • r1/mut/: mutant definitions, runner, per-mutant output and shims-left lists, summary.txt, and the manifest check after the runs. h22noreap ran after the first batch, so its line follows DONE.