diff --git a/docs/active-work.md b/docs/active-work.md index 32485b0..93b07d8 100644 --- a/docs/active-work.md +++ b/docs/active-work.md @@ -279,14 +279,18 @@ from #171 and #215. **`72da24a`**, worktree `/home/jeans/Repos/personal/pmacs-probe-sigint`. Recover with `git fetch githubsucks && git checkout gpu-probe-sigint-teardown`. -- **No PR. Framing revision 1 at `docs/gpu-probe-sigint-framing.md`; +- **No PR. Framing revision 2 at `docs/gpu-probe-sigint-framing.md`; NO IMPLEMENTATION and no fix proposed** — the mechanism is not known - yet, and the framing says so rather than guessing. + yet, and the framing says so rather than guessing. Revision 1 was + rejected on five findings, all upheld. - **Why it exists.** `ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` fails in gate stage `sweep-crdt` with "child did not exit within 5s". **Pre-existing on `main`** — `72da24a` fails it in a clean worktree - with its own target dir, 119 binaries green and one red. While it - reds, **no branch can present a green sixteen-stage gate, `main` + with its own target dir. The correct count is **119 green result + summaries and TWO red binaries**: `gpu_initial_target_acceptance` + includes the suite as a module, so a reproducing sweep reds twice + (log `…-2144707/09-sweep-crdt.log:3097` and `:3131`). While it reds, + **no branch can present a green sixteen-stage gate, `main` included.** - **`panel-mapping-generation` (§5b) is HELD BEHIND THIS LANE** by explicit instruction. That lane is code-complete at `5174f73` with @@ -305,19 +309,43 @@ from #171 and #215. `pmacs-gpu/src` for signal machinery returns nothing. The probe polls at 50ms (`pmacs-gpu/src/main.rs:1065`). Two processes with default `SIGINT` disposition should both die at once. -- **One retracted claim, kept as a warning.** An earlier "mechanism - located" report — launcher in `do_wait`, probe child in - `futex_do_wait` — was retracted on its own evidence: the sampler - caught 394 launchers with a maximum lifetime of 5s, while the failing - instance must live 8s or more. It had described a HEALTHY teardown. - The first step is therefore an instrument recording **only** - launchers outliving ~6s, with `/proc//status` signal masks — - `SigIgn` survives `fork` AND `exec`, handlers do not. +- **TWO retracted claims, both mine.** (a) "Mechanism located" — + launcher in `do_wait`, probe child in `futex_do_wait`. (b) The + retraction of (a), which argued the failing launcher "must live ≥8s". + **(b)'s arithmetic is false**: both reproducing binaries finish in + ~5.19s *including* the 5s timeout, so the failing launcher lives + about **5.1s** — inside what the sampler saw, and a ">6s" selector + would have captured nothing, repeating the error it was meant to + correct. (a) is therefore not refuted by (b); it stays **unproven for + a different reason** — the suite spawns launchers from five call + sites, so command line alone cannot attribute one to this test. + **Do not key on process age. Key on the PID the test records.** +- **Diagnostics must DISCRIMINATE** blocked delivery, inherited ignore, + and an escaped process group: snapshots **before and after** the + signal, for test parent / launcher / probe; **per-thread** `SigBlk` + from `/proc//task/*/status`; `SigPnd`/`ShdPnd`; and + `PID`/`PPID`/`PGID`/`SID`. A post-failure snapshot cannot prove + inheritance. +- **"Neither binary handles signals" does NOT mean default + disposition.** `SIG_IGN` is inherited across `fork` and survives + `exec`, so an inherited non-default disposition is the leading + hypothesis precisely because the source is silent. Revision 1's + "two processes with default disposition" claim contradicted its own + hypothesis and is withdrawn. +- **Reduction evidence is enumerated** in the framing §4 with command, + run count and log for each of R1–R10 and F1–F5, with the logs + preserved off the tmpfs at + `/home/jeans/build/pmacs-gate-targets/probe-sigint-evidence/` — + `/tmp` is a tmpfs and these were nearly lost to a cleanup mid-lane. +- **Coherence: journey step 12(a), "closing is clean", IS touched** — + Ctrl-C teardown of a GPU session is that step, grade movement or not. + Revision 1 claimed no journey step, reasoning from grade movement, + which §20 explicitly warns against. - **Gates:** `./scripts/gate --protocol --acceptance - gpu_invocation_acceptance` at minimum; acceptance criterion A2 - requires `sweep-crdt` green **three consecutive times** before the - fix is believed, since 1/1 is not evidence for a defect that hid from - every reduction. + gpu_invocation_acceptance` at minimum; A2 requires `sweep-crdt` green + **three consecutive runs on the reviewed fixed head of this branch** + — not "on main", which is unobtainable before approval and merge. + 1/1 is not evidence for a defect that hid from every reduction. ## `scripts/gate` TMPDIR isolation — PR #240 OPEN diff --git a/docs/gpu-probe-sigint-framing.md b/docs/gpu-probe-sigint-framing.md index 3f6ca3a..9bc587f 100644 --- a/docs/gpu-probe-sigint-framing.md +++ b/docs/gpu-probe-sigint-framing.md @@ -1,11 +1,15 @@ # GPU launcher / probe SIGINT teardown — framing -Revision 1. Status: **awaiting approval. No implementation.** +Revision 2. Status: **awaiting approval. No implementation.** + +Revision 1 was rejected on five findings. Each is answered below, and +the two that changed the technical picture — the lifetime arithmetic +(§5) and the disposition claim (§3) — are recorded as corrections +rather than quietly rewritten. ## 1. The problem, stated as what is observed -`gpu_invocation_acceptance::crdt::ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` -fails inside the `sweep-crdt` gate stage with +`ctrl_c_on_launcher_group_does_not_reach_spawned_daemon` fails with ``` child did not exit within 5s (tests/gpu_invocation_acceptance.rs:180, called from :1115) @@ -13,148 +17,209 @@ child did not exit within 5s (tests/gpu_invocation_acceptance.rs:180, called f The test spawns `pmacs --gpu --socket ` in its own process group (`process_group(0)`, `:1107`), waits for the probe to report -`phase=ready`, sends `SIGINT` to the **group**, and requires the -launcher to exit within five seconds. +`phase=ready`, sends `SIGINT` to the **group** (`:1113`), and requires +the launcher to exit within five seconds. -This is **pre-existing on `main`**. It is not caused by any feature -branch: the identical `build-crdt && sweep-crdt` pair run at `72da24a` -in a clean worktree with its own target directory fails the same test — -119 test binaries green, one red. +**It fails in two binaries, not one.** `tests/gpu_initial_target_acceptance.rs` +includes the suite as a module, so a reproducing sweep reds twice. +Reference run `20260816T064549Z-2144707/09-sweep-crdt.log`: + +| | line | result | +|---|---|---| +| `gpu_initial_target_acceptance` | 3097 | `FAILED. 14 passed; 1 failed … 5.19s` | +| `gpu_invocation_acceptance` | 3131 | `FAILED. 14 passed; 1 failed … 5.18s` | +| green result summaries | — | **119** | + +So the correct statement is **119 green result summaries and two red +binaries**. Revision 1 said "119 binaries green, one red", which was +wrong on both halves. + +**This is pre-existing on `main`.** The identical `build-crdt && +sweep-crdt` pair at `72da24a`, clean worktree, own target directory, +fails the same test. ## 2. Why this blocks more than one lane -`sweep-crdt` is stage 15 of the sixteen-stage `--protocol` gate. While it -reds, **no branch can present a green gate**, `main` included. The -`panel-mapping-generation` (§5b) lane is complete with its own fifteen +`sweep-crdt` is stage 15 of the sixteen-stage `--protocol` gate. While +it reds, **no branch can present a green gate**, `main` included. +`panel-mapping-generation` (§5b) is complete with its own fifteen stages green and is held behind this lane by explicit instruction. -## 3. Ground truth (cited, not recalled) +## 3. Ground truth (cited), and what it does *not* establish -- **The launcher does not handle signals.** `run_gpu` - (`src/main.rs:324`) builds a `Command` for the GPU binary and blocks - in `command.status()` (`:363`) — a plain `waitpid`. It installs no - handler, so `SIGINT`'s default action should terminate it outright. -- **`pmacs-gpu` does not handle signals either.** Grepping +- **Neither binary contains signal-handling code.** `run_gpu` + (`src/main.rs:324`) blocks in `command.status()` (`:363`) — a plain + `waitpid` — with no handler installed. Grepping `SIGINT|signal_hook|sigaction|ctrlc|set_handler|pthread_sigmask|sigprocmask` - across `pmacs-gpu/src` returns **nothing**. The probe should also die - on the default action. -- **The probe never blocks indefinitely.** `run_headless_managed_probe` - (`pmacs-gpu/src/main.rs:1065`) loops on - `event_rx.recv_timeout(Duration::from_millis(50))`, so it wakes twenty - times a second regardless of traffic. -- **The daemon *does* handle signals**, and deliberately: - `src/daemon.rs:629-641` registers `SIGTERM`/`SIGINT` through + across `pmacs-gpu/src` returns nothing. +- **The probe never blocks indefinitely.** + `run_headless_managed_probe` (`pmacs-gpu/src/main.rs:1065`) loops on + `event_rx.recv_timeout(Duration::from_millis(50))`. +- **The daemon *does* handle signals, deliberately.** + `src/daemon.rs:629-641` registers `SIGTERM`/`SIGINT` via `signal_hook::flag`. The daemon is the process the test asserts must - **survive**; it is detached from the launcher's group, which is the - property under test. -- **Only one test in the suite uses `process_group(0)`** — this one. + **survive**, detached from the launcher's group. -Taken together the ground truth **deepens** the puzzle rather than -explaining it: two processes with default `SIGINT` disposition, one of -them polling at 50 ms, should both die immediately. +**Correction to revision 1.** Revision 1 concluded from the first two +bullets that "two processes with default `SIGINT` disposition should +both die at once". **That does not follow, and it contradicted this +document's own leading hypothesis.** Absence of handler *code* says +nothing about runtime *disposition*: `SIG_IGN` is inherited across +`fork` **and** survives `exec`, so either process can hold a +non-default disposition it never installed — from the test harness, +from `cargo`, or from the invoking shell. Inherited ignore is a live +candidate precisely because the source is silent. What the source +establishes is narrower: **neither binary sets a disposition itself**, +so whatever disposition they hold at runtime was inherited, and that is +measurable rather than arguable. -## 4. What has been ruled out, each by measurement +## 4. Reductions attempted — each with command, count, and log -Recorded so this lane does not re-run them. +Preserved off the tmpfs at +`/home/jeans/build/pmacs-gate-targets/probe-sigint-evidence/`, because +`/tmp` is a tmpfs and these were nearly lost to a cleanup mid-lane. -| hypothesis | how it was refuted | -|---|---| -| machine load | red on a quiet box, load 2.77 at launch | -| tmpfs starving RAM | `/tmp` 21 G → 1.2 G, available 27 G → 45 G; still red | -| leaked test daemons | peak 58, only +8 per sweep; green runs already sat at 46–60 | -| inotify exhaustion | 47 instances in use of 1024 | -| `--workspace` feature unification | same two targets under `--workspace` are green | -| a specific preceding test | **all 37 preceding targets + the suite run green** | -| later targets / other packages | they run at log lines 4848+, after the failure at 3066 | +| # | reduction | runs | result | log | +|---|---|---|---|---| +| R1 | `cargo test --features crdt --test gpu_invocation_acceptance ctrl_c_on_launcher_group` | 3 | green, 0.15–0.17 s | *(console; superseded by R2)* | +| R2 | `cargo test --features crdt --test gpu_invocation_acceptance` (whole suite) | 1 | green, 15 passed | `gpu3.log` | +| R3 | `cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright ctrl_c_on_launcher_group` | 1 | green — every binary runs, only this test executes | `filtered.log` | +| R4 | `cargo test --features crdt --lib --test gpu_invocation_acceptance --no-fail-fast` | 1 | green, 2145 + 15 | `two.log` | +| R5 | `--test gate_script_acceptance --test gpu_invocation_acceptance` | 1 | green | `suspect.log` | +| R6 | three GPU suites in sweep order (`gpu_font`, `gpu_initial_target`, `gpu_invocation`) | 1 | green | `gpu3.log` | +| R7 | targets 1–19 (incl. `--lib --bins`) + the suite | 1 | green | `half1.log` | +| R8 | targets 20–37 + the suite | 1 | green | `half2.log` | +| R9 | **all 37 preceding targets** + the suite | 1 | green | `prefix.log` | +| R10 | `--workspace` with only `gpu_initial_target` + `gpu_invocation` | 1 | green | `wsonly.log` | +| F1–F5 | full `cargo test --workspace --features crdt --no-fail-fast -- --skip basedpyright` | 5 | **red, 5/5** | `base-sweep.log` (at `72da24a`), `postclean.log`, `sweep-inst.log`, `sweep-diag.log`, gate `…-2144707` | -**The last row is the strange one and is the real shape of this -problem**: the same binaries, in the same order, with the same tests -before it, pass as a subset and fail as part of the whole 119-target -sweep. Reproduction is 5/5 in the full sweep across two trees, and 0/N -in every reduction attempted. +**R9 is the shape of the problem.** The same binaries, in the same +order, with the same tests before it, pass as a subset and fail as part +of the whole. R10 rules out `--workspace` feature unification; other +packages' targets run at log lines 4848+, after the failure at 3066, so +they cannot be implicated either. -## 5. One retracted claim, kept as a warning +Also refuted, by measurement: machine load (red on a quiet box, load +2.77); tmpfs starving RAM (**tested by experiment** — `/tmp` 21 G → +1.2 G, available 27 G → 45 G, still red); leaked daemons (peak 58, +8 +per sweep, green runs already at 46–60); inotify (47 of 1024). -An earlier attempt reported the mechanism as "the launcher blocks in -`do_wait` on a probe child stuck in `futex_do_wait`". **That was -retracted on its own evidence.** The sampler behind it caught 394 -launchers and the longest-lived was **5 s total**; the failing instance -must outlive its `SIGINT` by 5 s, so its lifetime would be 8 s or more. -What was described is a *healthy* teardown from another test in the -suite. The pair is worth keeping only as the normal shape. +## 5. Two retracted claims, both mine, kept as warnings -The lesson binds this lane's first step: **an instrument that samples -every launcher and hopes to catch the failing one is not good enough.** +**Claim A — "mechanism located".** Reported the launcher blocked in +`do_wait` on a probe child in `futex_do_wait`. + +**Claim B — the retraction of A.** Argued A was unsupported because the +failing launcher "must live ≥ 8 s" while the sampler's longest-lived +was 5 s. + +**Claim B's arithmetic is false.** Both reproducing binaries finish in +**~5.19 s including the five-second timeout** (`:3097`, `:3131`), so +`phase=ready` is reached in roughly a tenth of a second and the failing +launcher lives about **5.1 s total** — squarely inside what the sampler +observed. A ">6 s" selector would therefore have captured **nothing**, +repeating the very sampling error it was written to correct. + +So A is **not** refuted by B. A remains **unproven for a different +reason**: the suite spawns launchers from **five** call sites +(`:38, :65, :509, :534, :544, :574, :725, :1097` — eight `--gpu` +arguments across the file), so a launcher captured by command line +alone cannot be attributed to *this* test. The `do_wait` / +`futex_do_wait` pair is consistent with the failing instance and +consistent with a healthy sibling, and nothing recorded distinguishes +them. + +The standing lesson is now the opposite of revision 1's: **do not key +on process age at all.** Key on identity. ## 6. Bets -1. The failure is a real teardown defect reachable by a user pressing - Ctrl-C, not a test artifact — because the test asserts an ordinary - product property and the two binaries involved carry no signal - handling at all. +1. **The failure is a real teardown defect** — a user pressing Ctrl-C + on `pmacs --gpu` sees the same hang. **This is a bet, not a + finding**, and the current witness does not reach the real GUI + path: it goes through a wrapper script and `--headless-managed-probe` + (`:1090-1093`), not a live wgpu frontend. Confirming or dropping this + bet is D4 below. 2. It is **not** a timing margin. A green run finishes in 0.15 s against - a 5 s deadline (33×). A margin that large does not erode; something - different happens. -3. Therefore **raising the deadline is not a fix** and is explicitly out - of scope. If the conclusion turns out to be that the deadline is - wrong, that requires its own argument and its own approval. + a 5 s deadline — 33×. Margins that large do not erode. +3. Therefore **raising the deadline is not a fix** and is out of scope. + If the conclusion turns out to be that the deadline is wrong, that + needs its own argument and its own approval. -## 7. First step — an instrument that keys on the failing instance +## 7. First step — diagnostics keyed on identity, not age -No fix is proposed yet, because the mechanism is unknown. The first -commit is diagnostic only: +No fix is proposed; the mechanism is unknown. The first commit is +diagnostic only, and it must **discriminate** the three live candidates: +blocked delivery, inherited ignore, and an escaped or wrong process +group. -- **D1.** Sample only launchers whose lifetime exceeds ~6 s, so the - failing instance is the *only* thing recorded, and capture for it: - `/proc//status` (`SigBlk`, `SigIgn`, `SigCgt`, `State`), the - per-thread `wchan` under `/proc//task/*/wchan`, and the same for - every child. `SigIgn`/`SigBlk` answers directly whether `SIGINT` was - ignored or blocked — including whether it was **inherited**, since - `SIG_IGN` survives both `fork` and `exec` while handlers do not. -- **D2.** Record whether the launcher had already reaped its child at - the moment the deadline expired, which separates "the child will not - die" from "the launcher will not notice". -- **D3.** Run the full sweep under D1/D2 until the failure is captured - **with** its diagnostics, and only then propose a fix. +- **D1 — key on the PID this test records.** The test already owns + `launcher.id()`. Capture around its own `kill`, not by scanning for + age or command line. +- **D2 — snapshot before *and* after the signal**, for the test parent, + the launcher, and the probe: + - `SigIgn`, `SigCgt`, `SigBlk` — **per thread**, from + `/proc//task/*/status`, since `SigBlk` is thread-specific and + a process-wide reading would hide a blocked delivery on the one + thread that matters; + - `SigPnd` and `ShdPnd` — a pending-but-undelivered `SIGINT` is + exactly what distinguishes blocked delivery from ignore; + - `PID`, `PPID`, `PGID`, `SID` for each — which settles whether the + signal was even addressed to the right group, and whether anything + escaped it. + A post-failure snapshot alone cannot prove inheritance; the + before/after pair is what makes the claim provable. +- **D3 — run the full sweep under D1/D2 until the failure is captured + *with* its diagnostics.** Only then propose a fix. +- **D4 — settle bet 1 separately.** Establish whether a real + `pmacs --gpu` session, not the wrapper/headless probe, reproduces the + hang. The answer decides whether A5 is an obligation or is dropped. ## 8. Acceptance criteria for the eventual fix -Deliberately written now, so the fix cannot quietly become "make the -test pass". +Written now so the fix cannot quietly become "make the test pass". -- **A1.** The named mechanism is stated and demonstrated, not inferred: - a witness that fails before the change and passes after, plus a - mutation showing the witness bites its own clause. -- **A2.** `sweep-crdt` green on `main` for **three consecutive full - runs** — 1/1 is not evidence for a defect that hid from every - reduction. -- **A3.** The reduction paradox is explained or explicitly recorded as - unexplained. If the fix makes the sweep green without accounting for - why subsets always passed, that gap is stated in the record rather - than left for the next reader to rediscover. -- **A4.** No deadline is raised, and no test is skipped, retried, or - serialised to obtain green. -- **A5.** If the mechanism proves to be in `pmacs-gpu`'s shutdown path, - a Ctrl-C on a real `pmacs --gpu` session tears down the frontend and - leaves the daemon running — the product property the test encodes. +- **A1.** The mechanism is stated and demonstrated, not inferred: a + witness failing before the change and passing after, plus a mutation + showing the witness bites its own clause. +- **A2.** `sweep-crdt` green for **three consecutive full runs on the + reviewed fixed head of this branch**. Not "on main" — that is + unobtainable before this lane is approved, gated and merged, and + revision 1 stated an impossible precondition. Post-merge + confirmation on `main` is a follow-up, not a gate on the fix. +- **A3.** The R9 paradox is explained, or explicitly recorded as + unexplained. A fix that greens the sweep without accounting for why + every subset passed leaves a gap, and the gap is stated rather than + left for the next reader. +- **A4.** No deadline raised, no test skipped, retried, or serialised + to obtain green. +- **A5.** **Conditional on D4.** If bet 1 holds, this is unconditional: + Ctrl-C on a real `pmacs --gpu` session tears down the frontend and + leaves the daemon running. If D4 shows the hang is reachable only + through the wrapper/headless path, bet 1 is dropped, A5 is struck, + and the lane is recorded as gate-correctness only. ## 9. Coherence impact (`COHERENCE.md` §20) -- **Journey steps touched: none.** This is a teardown-correctness and - gate-trustworthiness lane; no journey step changes grade. +- **Journey step touched: 12(a), "closing is clean."** Ctrl-C teardown + of a GPU session is exactly that step, whether or not its grade + moves. **Revision 1 said "journey steps touched: none", which was + false** — it reasoned from grade movement, which §20 explicitly warns + against. +- **Grade movement: none expected.** This restores a property that is + supposed to hold, rather than opening a new one. - **Interaction islands: none added.** -- **Config registry: not touched.** -- **Background-work attribution: not touched.** - -Naming these explicitly matters: a reader auditing §20 by grade movement -would otherwise conclude this lane touches nothing, when what it -restores is the ability of every other lane to prove itself. +- **Config registry: not touched. Background-work attribution: not + touched.** +- Beyond step 12(a), what this lane restores is every *other* lane's + ability to prove itself, since no branch can show a green gate while + stage 15 reds. ## 10. Out of scope - Raising or removing the 5 s deadline (bet 3). -- The ~10 daemons each sweep leaks. Real, separately recorded, and not - implicated here — green runs already ran at 46–60 leaked daemons. -- The `gpu_initial_target_acceptance` binary including this suite as a - module, which makes a reproducing sweep report the failure twice. A - tidiness question, not a correctness one. +- The ~10 daemons each sweep leaks — real, separately recorded, and not + implicated: green runs already ran at 46–60 leaked daemons. +- `gpu_initial_target_acceptance` including the suite as a module. It + is why the failure reds twice, and it is a tidiness question, not a + correctness one.