From 7e12c82506a300e58ae50442413600b577ed3509 Mon Sep 17 00:00:00 2001 From: Merlin Date: Sun, 11 Oct 2026 19:59:42 +0000 Subject: [PATCH] =?UTF-8?q?Q32=20DONE:=20the=20G8=20two-run=20agreement=20?= =?UTF-8?q?check=20is=20deterministic=20=E2=80=94=20declared=20scrub=20set?= =?UTF-8?q?=20covers=20every=20clock-derived=20field=20(age=5Fseconds,=20l?= =?UTF-8?q?ast=5Factive=5Fat,=20last=5Fcheckin=5Fat,=20the=20newest-file?= =?UTF-8?q?=20pointer),=206=20consecutive=20green=20gate=20invocations,=20?= =?UTF-8?q?8=20paired=20runs=20with=20zero=20differing=20leaf=20paths;=20c?= =?UTF-8?q?ollector=208a477376=20unchanged,=20adoption=20identity=20is=20n?= =?UTF-8?q?ow=20collector=208a477376=20+=20selftest=209ce6a4e9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WORK-QUEUE.md | 22 ++++++ checkins/2026-10-11-merlin.md | 79 ++++++++++++++++++++ recoveries/helm-g8-scrub.md | 133 ++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 recoveries/helm-g8-scrub.md diff --git a/WORK-QUEUE.md b/WORK-QUEUE.md index 3a739a5..ee289ba 100644 --- a/WORK-QUEUE.md +++ b/WORK-QUEUE.md @@ -4314,3 +4314,25 @@ zero differing leaf paths across ≥5 pairs. Scratch tree: `/root/crucible/q31/` `8a477376…` (collector) and `1d58b823…` (selftest) until Alexander's `cp`. **Status: OPEN — named to the Helm lane**, ahead of the Q31 `cp` decision so that the gate that ships is the one that runs green deterministically. + +**Status: DONE 2026-10-11 (Merlin, Run 8) — `recoveries/helm-g8-scrub.md`. The gate is deterministic: 6 consecutive +green invocations (150 passed / 0 failed each), 8 paired runs with zero differing leaf paths, and the defect measured +wider than stated.** Root cause went beyond `age_seconds`: the pre-fix paired probe (age-only patch) was still 7 of 8 +pairs red on `profiles[].last_active_at` (`helm-collect.py:208`, newest mtime over `sessions//cron/output//logs/` — +changes whenever a live session writes) and `profiles[].evidence.source_path` (`:200`/`:212` — not a citation but the +**pointer to that newest file**, volatile by construction), plus `members[].last_checkin_at` (`:493`, same class). The +fix is selftest-only, the collector untouched: `SCRUB_KEYS` is declared and every key is verified present as a literal +in the collector source; the pointer rule nulls `evidence.source_path` only inside dicts carrying a live mtime reading, +with a check proving 12 stable citations **survive**; every key carries its fresh-collect live count in the check name +and the pre-Q32 scrub runs as a control proving the old set kept what the new one nulls. Control measurement, same 8 +pairs under the pre-Q32 scrub: 12 differing leaf paths, 4 of 8 pairs red. Gate: `HELM_COLLECTOR=/root/crucible/q31/ +helm-collect.py python3 helm-selftest.py` → 6 runs, each exit 0 / `helm-selftest: 150 passed, 0 failed` / `VERDICT: ALL +GATES PASS` (137 standing + 13 new Q32 assertions; G8 now 31 checks), one log digest per run in the deliverable. Diff +`helm-selftest-q32-before.py` `1d58b823…` → `helm-selftest.py` `9ce6a4e971525fdd…` (+115/−5, `DIFF-Q32.txt`). Live +snapshot delta 0 throughout (`aa842b0f…`, 14,039 B, mtime_ns `1791479579475780303`); deployed bin unchanged +(`ee0b36a8…` collector, `7ed1bca8…` selftest — nothing deployed); collector artifact `8a477376…` unchanged. Limits +stated in the deliverable: the enumeration is source-verified, not proven complete (the gate fails closed on any new +volatile field); the `evidence.source_path`-as-mutable-pointer design smell is a collector-side follow-up, not made +here. **Adoption ask (Alexander, §4):** `cp` the collector `8a477376…` plus the selftest **`9ce6a4e9…`** (superseding +`1d58b823…`) into `/root/.helm/bin/` — Q31's adoption is unblocked behind it; deploy/schedule of collect+render still +his call. diff --git a/checkins/2026-10-11-merlin.md b/checkins/2026-10-11-merlin.md index 638e9f1..d4c347d 100644 --- a/checkins/2026-10-11-merlin.md +++ b/checkins/2026-10-11-merlin.md @@ -1002,3 +1002,82 @@ edited; no secret printed. — Merlin + +--- + +# Run 8 — Q32: the G8 two-run agreement check is deterministic (Merlin, 2026-10-11 ≈19:5x–20:2x UTC) + +> Screen. Every claim below carries a path, a digest, or a command with its output. No secret is +> printed here or in the files this lands. + +## Did + +Claimed and finished **Q32 — the G8 two-run agreement check is nondeterministic** (the queue's only +OPEN item, named to the Helm lane; Q20 is zephyrus-sparkborn's, Q9 blocked on the audio handover, +Q13/Q19 gated on Alexander). Deliverable: `recoveries/helm-g8-scrub.md`, scratch tree +`/root/crucible/q31/` (re-runnable: `q32-probe.py`, `q32-paired.py`). + +**Root cause, and it is wider than the item stated.** The scrub covered `observed_at`, +`rtt_seconds`, `generated_at` and `collector.duration_seconds` — and missed `age_seconds` +(`helm-collect.py:545`, `round(age, 1)` from the run-end clock, on every source block). But a +paired-run probe with an age-only patch was **still 7 of 8 pairs red**, on +`profiles[].last_active_at` (`:208` — newest mtime over `sessions//cron/output//logs/`, which +changes whenever a live session writes), `profiles[].evidence.source_path` (`:200`/`:212` — not a +citation but the **pointer to that newest file**, volatile by construction) and +`members[].last_checkin_at` (`:493`, same class). + +**The fix, selftest-only — the collector artifact `8a477376…` is untouched.** `SCRUB_KEYS` is +declared and every key is verified present as a literal in the collector source; the pointer rule +nulls `evidence.source_path` only inside dicts carrying a live mtime reading, with a check proving +**12 stable citations survive** the scrub; every key prints its fresh-collect live count in the +check name and the pre-Q32 scrub runs alongside as a control proving the old set kept what the new +one nulls. Diff `1d58b823…` → `9ce6a4e971525fdd…`, +115/−5 (`DIFF-Q32.txt`). + +**Results, measured:** + +- **8 paired runs, 0 differing leaf paths** under the gate's own `_scrub` (extracted from the + selftest source by AST and exec'd — the paired script cannot drift from the gate). Control, the + same 8 pairs under the pre-Q32 scrub: **12 differing leaf paths, 4 of 8 pairs red**. +- **6 consecutive gate invocations**, `HELM_COLLECTOR=/root/crucible/q31/helm-collect.py python3 + helm-selftest.py`, each exit 0 / `helm-selftest: 150 passed, 0 failed` / `VERDICT: ALL GATES + PASS` (137 standing + 13 new Q32 assertions; G8 now 31 checks). One log digest per run in the + deliverable: `bfcc35cd92484acb`, `0a6272e3f6d88a96`, `bcf24e51049e661e`, `b9740dbd33f9f494`, + `f69d56ef176b2eaf`, `4bf31912f396161d`. + +**Nothing touched.** Live snapshot `/root/.helm/data/snapshot.json` delta 0 throughout +(`aa842b0f…`, 14,039 B, mtime_ns `1791479579475780303`, before and after every run); deployed bin +unchanged (`ee0b36a8…` collector, `7ed1bca8…` selftest — nothing deployed); live ledger index +`https://alexanderwhitestone.com/stackchain/blocks/` 200 / 34,488 B / +`4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`; `decisions/` and `inbox/` +untouched; nothing executed; no another-party file edited. + +## Blocked + +Nothing blocked the run. Limits, stated in the deliverable: the scrub enumeration is +source-verified, not proven complete (the gate fails closed on any *new* volatile field — a pair +goes red and names the leaf — but nothing proves the seven literals are all the clock-derived keys +the collector will ever emit); the `evidence.source_path`-as-mutable-pointer design smell is a +collector-side follow-up, not made here, because the collector is the artifact under adoption and +Q32 deliberately does not change it; the gate runs with `--skip-tests` (the standing Q31 caveat). + +## Next + +- **Alexander's call, §4:** adopt the gated collector `8a477376…` **plus the selftest + `9ce6a4e9…`** (superseding `1d58b823…`) into `/root/.helm/bin/` — Q31's adoption is unblocked + behind it — then the deploy/schedule decision for collect+render. Same gate as Q26–Q30. +- **Q7, measured:** after Q32 the queue's only OPEN item is none; Q20 is zephyrus-sparkborn's named + lane, Q9 blocked on the audio handover, Q13/Q19 gated on Alexander. The unclaimed executable set + is empty again, so a successor is owed; the natural candidate is the collector-side redesign of + the newest-file pointer (`evidence.source_path` as a stable citation plus a named + `latest_active_path` field) — this run's own residue, so per the standing pattern I open none and + name it instead. +- The deploy-or-retire ask for the honest Helm page (Run 2) and the adopt-or-hold asks on the + patched builder (Q26–Q29) all still stand unchanged. + +## Ask + +1. **Alexander — adopt the gated collector + the fixed selftest?** The identity pair is now + `8a477376…` (collector, unchanged) and `9ce6a4e9…` (selftest, the Q32 fix). It is the difference + between a gate whose red is a coin flip and one with 6 consecutive green invocations behind it. + Scratch until you say so. +2. **Nothing asked of zephyrus-sparkborn this tick.** His lane is Q20. diff --git a/recoveries/helm-g8-scrub.md b/recoveries/helm-g8-scrub.md new file mode 100644 index 0000000..46020ce --- /dev/null +++ b/recoveries/helm-g8-scrub.md @@ -0,0 +1,133 @@ +# Q32 — the G8 two-run agreement check is deterministic + +> Screen. Every claim below carries a path, a digest, or a command with its output, measured this run +> (2026-10-11 ≈19:5x–20:2x UTC) from this host on the scratch tree `/root/crucible/q31/`. No secret is +> printed here or in the files this lands. + +## The defect + +G8 asserted "two CLI runs agree byte-for-byte once the clock fields are scrubbed", comparing two real +subprocess collects. The scrub covered `observed_at`, `rtt_seconds`, the top-level `generated_at` and +`collector.duration_seconds` — and nothing else. `helm-collect.py:545` stamps +`age_seconds = round(age, 1)` on **every source block** from the run-end clock, so any run pair +straddling a 0.1 s boundary in a source's age disagreed on that leaf alone. A gate whose red is a coin +flip cannot be cited as evidence that an artifact is safe, and the Q31 adoption ask rests on exactly +that citation. + +## Root cause, and it is wider than `age_seconds` + +Before patching, a paired-run probe ran with an `age_seconds`-only patch: **7 of 8 pairs still red**, +on two further volatile fields the scrub also missed: + +- `profiles[].last_active_at` (`helm-collect.py:208`) — newest mtime across `sessions/`, + `cron/output/`, `logs/`. Changes whenever a live session writes a file, i.e. constantly on this host. +- `profiles[].evidence.source_path` (`helm-collect.py:200`, `:212`) — **not a citation but a pointer**: + the path of that newest file. Volatile by construction wherever a live mtime reading exists. Every + *other* `evidence.source_path` in the snapshot is a stable citation. +- `members[].last_checkin_at` (`helm-collect.py:493`) — check-in file mtime, the same class (the + `checkins/` directory is written by other agents). It did not differ in this run's 8 pairs; it is + the same class and is covered. + +## The fix — selftest only, the collector is untouched + +- `SCRUB_KEYS = {observed_at, rtt_seconds, age_seconds, last_active_at, last_checkin_at}`, **declared**. + A gate check verifies every key appears as a literal in the collector source (with `generated_at` and + `duration_seconds` handled explicitly at the top level), and a second check that the declared set is + exactly the clock literals minus the two top-level keys. +- **The pointer rule:** `evidence.source_path` is nulled inside any dict carrying `last_active_at` or + `last_checkin_at`. A gate check proves the carve-out is measured, not asserted: **12 stable + `evidence.source_path` citations survive the scrub** (the 24 newest-file pointers are nulled). +- **Non-vacuity per key:** each check name prints the fresh collect's live (non-None) value count, and + the **pre-Q32 scrub runs alongside as a control** proving the old set kept every value the new one + nulls. (`email.age_seconds` is legitimately `None` — UNREACHABLE source — so 5/6 is the honest count.) +- Diff: `helm-selftest-q32-before.py` (sha256 `1d58b823809aa76e…`) → `helm-selftest.py` + (sha256 `9ce6a4e971525fdd4e49150fbeb49e584b7eb9cad293164f060a6f2c47739562`), `DIFF-Q32.txt`, + **115 lines added / 5 removed**. + +## Results, measured + +**Paired runs** — `q32-paired.py`, 8 pairs, two real subprocess collects per pair, both bodies scrubbed +with the **gate's own `_scrub`** (extracted from `helm-selftest.py` by AST and exec'd, so the paired +script cannot drift from the gate), compared leaf by leaf: + +- **8 pairs, 0 differing leaf paths** under the gate's scrub. +- Control, same 8 pairs under the pre-Q32 scrub: **12 differing leaf paths, 4 of 8 pairs red** + (`sources.cron_jobs.age_seconds`, `sources.hermes_profiles.age_seconds`, + `profiles[N].last_active_at`, `profiles[N].evidence.source_path` on actively-written profiles). + Log: `q32-paired.txt`. + +**Gate invocations** — `HELM_COLLECTOR=/root/crucible/q31/helm-collect.py python3 helm-selftest.py`, +6 consecutive runs, each **exit 0, `helm-selftest: 150 passed, 0 failed`, `VERDICT: ALL GATES PASS`** +(137 standing + 13 new Q32 assertions; G8 is now 31 checks). One log's digest per run: + +| run | exit | passed / failed | log sha256 (first 16) | +|---|---|---|---| +| 1 | 0 | 150 / 0 | `bfcc35cd92484acb` | +| 2 | 0 | 150 / 0 | `0a6272e3f6d88a96` | +| 3 | 0 | 150 / 0 | `bcf24e51049e661e` | +| 4 | 0 | 150 / 0 | `b9740dbd33f9f494` | +| 5 | 0 | 150 / 0 | `f69d56ef176b2eaf` | +| 6 | 0 | 150 / 0 | `4bf31912f396161d` | + +The digests differ run to run **by construction**: the collector's stdout carries the digest of the +file it wrote, and the written body carries the unscrubbed clock fields. Each run's digest is the +evidence for that run. + +New assertions, with their fresh-collect counts from run 1's log (`q32-gate-run-1.log`): + +``` +PASS G8 two CLI runs agree byte-for-byte once the clock fields are scrubbed +PASS G8 every clock-derived key the collector emits appears in its source +PASS G8 the declared scrub set is exactly the clock literals minus the two top-level keys +PASS G8 the scrub normalises age_seconds (fresh collect: 5/6 non-None) +PASS G8 the pre-Q32 scrub kept age_seconds (the defect, as a control) +PASS G8 the scrub normalises last_active_at (fresh collect: 20/20 non-None) +PASS G8 the pre-Q32 scrub kept last_active_at (the defect, as a control) +PASS G8 the scrub normalises last_checkin_at (fresh collect: 4/4 non-None) +PASS G8 the pre-Q32 scrub kept last_checkin_at (the defect, as a control) +PASS G8 the scrub normalises observed_at (fresh collect: 36/36 non-None) +PASS G8 the scrub normalises rtt_seconds (fresh collect: 1/1 non-None) +PASS G8 the newest-file pointer evidence.source_path is scrubbed (fresh collect: 24/24 non-None) +PASS G8 the pre-Q32 scrub kept the newest-file pointer (the defect, as a control) +PASS G8 stable evidence.source_path citations survive the scrub (12 paths) +``` + +## Nothing touched + +- The gated collector `/root/crucible/q31/helm-collect.py` is unchanged at + `8a477376f72c578b0633e07741b44c717482fe62b29a3cc3f8abb7a6696ea76c`, verified before and after every + run this tick. Q32 changes the selftest only. +- The deployed `/root/.helm/bin/helm-collect.py` is unchanged at `ee0b36a82f23fcf3…` and + `/root/.helm/bin/helm-selftest.py` at `7ed1bca8f9da624f…` (the deployed gate is still the 119-pass + one; nothing was deployed). +- The live snapshot `/root/.helm/data/snapshot.json` is **14,039 B / sha256 + `aa842b0fd88037f83115b4a54d617f27666f4de06ef84f9a64c8fd0a5f04656d` / mtime_ns + `1791479579475780303`** — byte- and mtime-identical before and after the paired runs and all six + gate invocations (delta 0). +- The live ledger index `https://alexanderwhitestone.com/stackchain/blocks/` is unchanged: **200 / + 34,488 B / sha256 `4f764ef4dfb9c08b2654ff0783550d20ad1bb7a31cb0c32070974a5fe451557e`**. +- `blocks.json`, `decisions/` and `inbox/` were not touched; nothing was executed; no another-party + file was edited; no secret is printed in this record or in the files it lands. + +## Limits, stated not hidden + +- **The enumeration is declared and source-verified, not proven complete.** The gate fails closed on + any *new* volatile field (a pair goes red and the leaf path is named in the failure), but nothing + proves a priori that the seven clock literals are all the clock-derived keys the collector will ever + emit. +- **The pointer rule is a scrub convention; the design smell stands.** For a profile or a member, + `evidence.source_path` is a mutable newest-file pointer where every other `evidence.source_path` is + a stable citation. The clean fix is collector-side — point `source_path` at the profile directory + (or the member's file) and name the newest-file path its own field. The collector is the artifact + under adoption and Q32 deliberately does not change it. +- The gate runs with `--skip-tests` (the standing Q31 caveat): the ~2 s collector is deterministic and + does not invoke the Timmy suite. + +## Adoption + +Q32 changes only the selftest. The Q31 adoption ask becomes: `cp` the collector `8a477376…` +(unchanged) **plus the selftest `9ce6a4e9…`** (superseding `1d58b823…`) into `/root/.helm/bin/` — the +gate that ships is the deterministic one. Deploying collect+render, and scheduling it, remain +Alexander's under §4. + +— Merlin, 2026-10-11