Adopt AI_INFRA 2026-07-13 broadcast ab6c387e (presence-not-age selector, type=update) + directed 6c32df89 rec-3, via /fable-clone 5-lane reviewer ensemble (wf_b621aac4-f0b) -> spec -> /fable-real deep-pass (PASS-WITH-FIXES, M1+M2 applied) -> HMW execute (em-main solo; governance single-writer D9). D2 (hmw.js): unknown-role fail-soft-WARN -> up-front STOP-HARD throw (before parallel; preserves null/'' role-less inherit-lead path). node --check + stub 7/7. Doc-sync 5 live lines (ultra-on/README/harness-11-engine/runbook). D3 (memory-archive-gate.ps1): value_protect advisory-flag -> pre-selection HARD-SKIP (value-primary; heading-only spans; non-contiguous byte accum) + value-floor WARN. Fault-inject ALL PASS + real regression A7 242/242. DRY-RUN. D1 (reinject-ledger.md): presence re-verify stamp + 3 honest-notes + BUILD-GAP. Re-verify: reinject (i) + MFE age-band (iii) already COMPLIANT; only the archive-gate age-trace needed hardening. D3 = defense-in-depth codify, NOT leak-closure (9-token grep unchanged; em-main value-scan stays the guarantee). adap-report + email AI_INFRA (8b9dc5165d5a); inbox STAGE-2 processed. No new User-Mark (codify-only). #53 garble x1 (lane-4) recovered from disk, 0 loss. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
sub-reviewer-3 — LENS 3: FLOOR (iii) SELF-CHECK (drop-date/drop-order test)
Run: 2026-07-13-presence-not-age-adopt · Lane: reviewer 3 of 5 · Broadcast: ab6c387e (presence-not-age selector guardrail, type=update) + directed 6c32df89
FloorPoint owned: (iii) self-check — apply the broadcast's own drop-date-column test to ALL 3 SE selectors (reinject / archive-gate / mfe age-band).
SETTLED VERDICT — floorPoint (iii): RED-FLAG · necessity SHOULD
One of the three SE selectors trips the broadcast's own self-check. Enumeration:
| Selector | Drop date/order input → behavior change? | date/order used for… | Verdict |
|---|---|---|---|
REINJECT (reinject-ledger.md + CG-1) |
SELECTION: no · CG-1 debounce: yes | floor-rot = presence-gap; CG-1 = per-item last-seen debounce | COMPLIANT |
ARCHIVE-GATE (memory-archive-gate.ps1) |
YES | age-RANK (entry-ORDER = oldest-first drain) | RED-FLAG |
MFE age-band (mfe-eval.ps1) |
prints a different count only | advisory FLAG print (never cut/select) | COMPLIANT |
The RED-FLAG is driven entirely by the archive-gate planner. It is NOT a live incident (DRY-RUN + advisory value-flag + documented design-intent), hence SHOULD not MUST — but per the broadcast's own mechanical test it is the exact "sorts/filters by oldest-first to decide cool-down" pattern (iii) tells me to surface and redesign. Reporting it (not rationalizing it away) is the anti-Smart-Friend call.
SELECTOR 1 — REINJECT — COMPLIANT (with one honest-note)
Selection trigger is pure presence. reinject-ledger.md:3 — reinject fires for a "mục-sàn floor-rot (đã-từng-ở-hot-mem nay RỚT)"; :12 — "item ĐÃ-từng-tồn-tại ∩ CÒN-giá-trị ∩ RỚT-khỏi-L1". That is verbatim the broadcast (i) condition "should be present but is missing." No age term.
Drop-date test on the SELECTION: the floor-rot classifier tests (a) was-in-L1, (b) still-valuable, (c) dropped-from-L1. None is a date. Drop every date/session column → the what-to-reinject decision is unchanged. ✓ presence-based.
The one date-touch = CG-1 debounce, and it is the OK kind. :9 — "Mỗi item-id được reinject TỐI-ĐA 1 lần trong N phiên (N = 3)." Enforcing this needs the reinjected session column (:21). Drop it → the debounce can't count → behavior changes. BUT this is a per-item last-acted marker ("when did I last reinject item X?") used as a loop-breaker — it does NOT rank candidates against each other by age, and it never cuts "the oldest." Per the broadcast carve-out (ab6c387e:68: "date used only to determine PRESENCE (last-seen) … = OK") this is compliant. It is a debounce keyed to the last-reinject-event, not an age-rank selector.
Extra safety: ledger is a CONVENTION (:38 — em-main hand-appends, git-tracked; no OS-hook auto-write) and currently EMPTY (:34). No automated age-based selection exists here at all.
1-line re-verify note to keep in docs (broadcast §5): "reinject SELECTS by floor-rot presence-gap; CG-1's N=3 session-distance is a per-item last-seen debounce (loop-breaker), not an age-rank — re-verified, no change."
SELECTOR 2 — ARCHIVE-GATE — RED-FLAG (the finding)
Evidence — the planner selects by entry-ORDER (age-proxy), oldest-first
memory-archive-gate.ps1:
:137— "A4 hysteresis + A5 keep-floor : how many OLDEST entries to move?":138— "Move oldest entries one-by-one":153-154—for ($move = 1; $move -le ($entryCount - $keepFloor); $move++) { $cutLine = $markers[$move] # first KEPT entry starts here }→ the moved set is alwaysmarkers[1..moveCount]= the entries at the TOP of the file.:182— resolution string: "PROPOSE archive … move$moveCountoldest -> curate L1->L2 by hand":164-165— keep_floor protects the LASTkeepFloormarkers (bottom); the drain targets the top.
There is no literal date column — but entry-ORDER is the age-proxy (MEMORY.md convention = append-newest-to-end, memory-budget.json:24 l2_verbatim "curated L1 entries APPEND to end only"; keep_floor note "protect NEWEST-n" assumes newest=bottom). So "position from top" ≡ "age rank."
Drop-order test → FAILS as age-RANK
Remove the assumption that top=oldest (shuffle entry order / drop the position input): the planner proposes a different move-set. The order is consumed as a rank (drain from position 1 upward until under low-watermark), not as presence. This is precisely broadcast (iii)'s red flag: "any place that SORTS/FILTERS by … oldest-first to DECIDE cool-down = RED FLAG."
Worse for SE specifically: "oldest = top" is INVERTED here
SE MEMORY.md files are structured (evergreen header sections first, chronological tail last), not flat chronological logs. Verified against .claude/agent-memory/reviewer/MEMORY.md via the gate's own marker regex ^(#{2,3}\s|---\s*$):
- marker 1 =
:7## 📁 Area memory· marker 3 =:25## 🎯 Role baseline· marker 5 =:31## 🚨 Recurring bug patterns· marker 7 =:44## 📋 6-category checklist·:55## ⚠️ Anti-patterns + Smart Friend guard·:63## 🧠 review essentials - the chronological
## 📅 Recent activityonly starts at:75.
So "move the oldest (top) entries first" would propose archiving Area memory → Role baseline → Recurring bug patterns first — the highest-value evergreen content — while keep_floor protects the recent chronological tail at the bottom. The age-proxy is not merely age-based, it is anti-correlated with value for SE's file shape.
(Aside, secondary: --- separators are counted as markers too, so entryCount double-counts each section = the moveCount arithmetic is loose. Not my floor; noted for the code-quality lane.)
Why it is RED-FLAG and not COMPLIANT — the value-gate exists but is advisory-only
value_protect (:167-191) scans the moved prefix for high-value markers (memory-budget.json:40 patterns: gotcha# / anti-pattern / recurring / lost-update / race / lesson / guard / root-cause / silent-fail) and prints "KEEP in L1 regardless of age (do NOT age-archive)" (:190). This is the value-axis the broadcast (ii) wants — BUT:
- it is ADVISORY ONLY:
:55-56"ADVISORY FLAG ONLY - em-main decides (no auto-exclude); keep_floor stays the recency axis";memory-budget.json:39"the patterns below are an advisory grep-hint … NOT an enforced auto-exclude." - the move-set is still computed oldest-first; value_protect annotates it, it does not re-select. The automated selector's ranking axis remains age-proxy; value only enters as a human-read flag.
So the function "cut low-value not FIFO-by-date" is carried by em-main (human) + a print-flag, not by the selector. The selector itself still ranks by age. That is what trips (iii).
Concrete fix (redesign the spot to value-gated presence) — pseudocode
Promote value_protect from post-hoc annotation to pre-selection exclusion, so age-order only ever tie-breaks the LOW-VALUE pool:
# BEFORE draining: partition entries into protected vs movable
protectedIdx = { k : entry[k] matches any value_protect pattern } # value axis (age-blind)
movablePool = entries NOT in protectedIdx AND NOT in keep_floor(newest N)
# size-drain ONLY the movable (low-value) pool:
for entry in movablePool (order = size-management tie-break, not an age claim):
move entry; if bytes-after < lowMark: break
if bytes still > cap AND movablePool exhausted:
emit "cannot auto-drain by size: remaining over-cap is value-protected/keep-floor
-> condense high-value entries BY HAND (do NOT age-archive)"
Key change: a value-protected entry is never in the move-set, regardless of its file position. Then dropping/shuffling entry-order changes only which low-value entries drain for byte-size — it can never touch protected content → the drop-order self-check PASSES.
Measurable acceptance criteria
- Selection-exclusion, not annotation: re-run
memory-archive-gate.ps1on an over-cap sub where the over-cap is caused by value-marked content → planner outputs "cannot auto-drain by size (movable pool exhausted) → condense by hand", and the proposed move-set contains zero value_protect-matched lines (today it lists them as "oldest" then flags them). - Order-invariance of KEEP set: in a test MEMORY.md, take one high-value entry and (a) prepend it vs (b) append it. Both runs must keep it. (Today: prepended → lands in moved-prefix, only FLAGGED; appended → lands in keep_floor, protected. Order-dependent treatment of the SAME entry = the bug the fix removes.)
- No regression on pure-size drain: a sub over-cap purely from many LOW-value episodic entries still drains to below low-watermark, never below keep_floor.
SELECTOR 3 — MFE AGE-BAND — COMPLIANT (model of a correct FLAG)
mfe-eval.ps1:183-189:
:185—foreach ($d in $markDates) { if (($Today - $d).TotalDays -gt 30) { $oldN++ } }:186-187— "age-band (FLAG only - age=false-proxy) … >30d old but still Active=$oldN-> KEPT (status-driven, age-blind)"
Drop-date test: empty $markDates → the printed count changes (goes to 0 / "date=0"). But the load-bearing outputs are untouched:
- denominator
:164$denomCount = marks + AS + guards + recurring— the marks in it are gated by STATUS (:95if ($status -match 'Active-High' -or $status -match 'Active\b')), date-independent; - Coverage-FIT
:169-178, Goodhart anchor:191-200— no date input; - there is no cut/sort/select anywhere downstream of
$oldN. It is aWrite-Hostcount and nothing reads it.
Date is used purely as an advisory FLAG, never to rank or cut. Matches memory-budget.json:67 honest_caveats.age_band "age is a FLAG, never a cut … An item drops only on status-change … NOT by age." This is the correct presence/status-driven pattern the broadcast endorses.
1-line re-verify note: "MFE age-band is FLAG-only; dropping the date changes one diagnostic count, not the denominator/FIT/Goodhart or any selection — re-verified, no change."
Other date/sort sites reviewed and CLEARED (out of the refine-step selector floor)
memory-selfimprove-audit.ps1:292Sort-Object LastWriteTime -Descending→ picks$runDirs[0]= latest run-trace to spot-check HCV harvest-completeness (:279-285, sub-md + synthesis present?). Not a memory reinject/archive selector; and "pick the most-recent run to verify it got harvested" is a last-seen/presence use, not an age-rank-then-cut. Cleared.memory-archive-gate.ps1:106/mfe-eval.ps1:216Sort-Object Name→ alphabetical sub ordering, not date. Cleared.backup-sql.ps1:49(SQL .bak retention) andapplied-eval-nokey.ps1("NO AGE DIMENSION YET" scaffold note) → unrelated to the memory refine-step. Cleared.
No literal date-sort exists in any of the three refine-step selectors; the ONLY age-mechanism that reaches a keep-vs-cut decision is the archive-gate's entry-ORDER.
SELF-REFUTATION (strongest case AGAINST my RED-FLAG)
The broadcast is type=update, proactive-prevention (ONE occurrence elsewhere, already fixed — ab6c387e:80-90 + §5 note 1), and explicitly "floor = FUNCTION not FORM" (§5 note 3) with "already-presence-based → quick re-verify only" (§5 note 2). Under that lens the archive-gate arguably ALREADY meets the FUNCTION-floor:
- it is DRY-RUN (
:6-8,:293-295) — it NEVER actually cuts anything by age; the binding archival act is em-main (human); - the human is explicitly handed the value flags (
:190), so the real keep-vs-cut decision is value-gated; memory-budget.json:39already documents the governing principle as "archival cuts LOW-VALUE, NOT FIFO-by-date … mark RC-…10-29-11 applied to the memory layer" — i.e., design-intent is already presence/value;- the byte-drain's job is size management (get under cap); ordering the drain oldest-first while flagging high-value is a defensible size heuristic, not a claim "old=archive."
So one could settle archive-gate as COMPLIANT-with-a-note, and call my RED-FLAG an over-escalation of a documented, human-gated, dry-run advisory.
Rebuttal (why RED-FLAG stands, but only at SHOULD): The broadcast's (iii) test is deliberately mechanical: "any place that SORTS/FILTERS by … oldest-first to DECIDE cool-down = RED FLAG → redesign to presence." The planner literally sorts oldest-first (entry-order) to produce the archive proposal — that is the flagged pattern by definition, independent of dry-run. "DRY-RUN + advisory + human override" mitigates impact, but does not make the selector presence-based; the selector still ranks by age-proxy, and for SE's structured files that proxy is even anti-correlated with value. Anti-Smart-Friend honesty requires me to report that it trips rather than rationalize it as fine — and the broadcast itself prescribes the resolution I gave ("redesign that spot to presence/value"). Hence: RED-FLAG (it trips — surface it) at SHOULD necessity (impact contained by dry-run/human/value-flag, and this is a proactive update — so harden, don't hard-block). I do not lower the bar to "PASS with a shrug," nor over-raise to a MUST-block on a dry-run advisory.
Docs to keep (broadcast §5 three honest-notes — for the synthesized spec)
- Basis = ONE occurrence already fixed elsewhere (proactive-prevention, NOT a spreading SE incident).
- SPECIFIC-APPLICATION, not a new rule — SE reinject + age-band were already presence/flag-based (quick re-verify, no change); only the archive-gate needs a hardening.
- Floor = FUNCTION not FORM — the fix is "value-gate the selection," implementable in SE's own script shape; no obligation to copy hub structure/filenames.