Skip to content

fix(hygiene): background strategy trims only under pressure without contextHygieneEnabled - #79

Merged
alpertarhan merged 1 commit into
mainfrom
fix/background-hygiene-gate
Sep 27, 2026
Merged

alpertarhan merged 1 commit into
mainfrom
fix/background-hygiene-gate

Conversation

@alpertarhan

Copy link
Copy Markdown
Owner

fix: background strategy trims only under pressure without contextHygieneEnabled

Found by re-running the item-14 evidence commands (task-eval-reports/item14-cost-evidence-2026-09-25/COMMANDS.txt) on main. Results stay local; nothing new is committed under task-eval-reports/ (now ignored by default, the cited 09-25 directory stays tracked).

What was wrong

Since d219600 (P1-4 timing, #76) the break-even/cold rule applied to every session the turn_end handler considered enabled — including sessions enabled only by autoTriggerStrategy: "background" with contextHygieneEnabled: false, which had trimmed only under pressure before. Effects in the evaluator (--background-prep --cache-warming=idle, same flags as 09-25):

  • the no-compaction baseline arm started trimming: 5,963,248 → 5,507,252 synthetic input tokens at d219600 (bisected: 370a832 still 5,963,248 with 2 warms);
  • every arm's idle warm replays went 2 → 0: each committed edit rebuilds the agent's messages and Pi's warmer reports conversation context changed (probed via session.cacheWarmingStatus after each idle wait).

Fix

register-smart-context-tool.ts turn_end: without contextHygieneEnabled, a ready batch commits only under pressure (cause pressure), never at break-even, and no mark is held. Test: "trims only under pressure when the background strategy enables cleanup without contextHygieneEnabled" (below the gate: no entries, no mark, a cold request changes nothing; at the gate: cause: "pressure"). The timing fixture now opts into contextHygieneEnabled explicitly. Mutation: removing the guard fails exactly that test. Docs: configuration (background strategy row), evaluation (expected warm count for hygiene arms), CHANGELOG Fixed.

Post-fix evaluator (local, offline; vs the 09-25 report)

  • run A (all arms): identical shapes — 72/72/78/78 requests, prefix reuse 1 / 0.9962 / 0.9564 / 0.9444, 14/14 oracles.
  • run B (--background-prep --cache-warming=idle): no-compaction 75 requests, 2 warms, prefix 0.9909 (as 09-25); eesv 87 / 2 warms / 0.8548 (as 09-25); recoverable-hygiene 73 / 0 warms and hybrid 85 / 0 warms — by design now: cleanup is on in those arms and a break-even trim commits at the idle boundary.
  • run C (--cache-warming=streaming): exactly 1 warm per arm during the long tool run (as 09-25).
  • run D: same as B for its two arms; traces written.

Verification

See the receipts comment.

…ontextHygieneEnabled

Since d219600 the break-even/cold timing applied to every session the
turn_end handler considered enabled, including sessions enabled only by
autoTriggerStrategy "background" with contextHygieneEnabled off; those
had trimmed under pressure only. Restore that gate: without
contextHygieneEnabled a ready batch commits only under pressure (cause
"pressure"), never at break-even, and no mark is held.

Found by re-running the item-14 evaluator (task-eval --background-prep
--cache-warming=idle): the no-compaction baseline arm had begun trimming
(5,963,248 -> 5,507,252 synthetic input tokens) and every arm's idle warm
count dropped from 2 to 0 because each committed edit rebuilds the agent's
messages and Pi's warmer reports "conversation context changed".
@alpertarhan

Copy link
Copy Markdown
Owner Author

Receipts (local, env -i, private HOME/TMPDIR) at the PR head:

  • bun run typecheck: clean (src, scripts, tests, bench).
  • bun test: 1564 pass / 0 fail across 120 files.
  • bun run gate: 376 / 0, EESV adversarial release gate passed (26 suites).
  • bun run bench: exit 0 (plan trim over 120 archived reads: median 2.1 ms, p95 2.4 ms). bun run build: pass. bun run release:audit: pass (269 packed files).
  • dist/index.js sha256 cbcb4b1e474adb8acbef8daff3d31f25831ac5066858da3eb5273ff7bc6e671d.
  • Evaluator re-runs A–D (offline, scripted transport, all arms PASS 14/14): see the PR body; outputs kept under /tmp, not committed.

@alpertarhan
alpertarhan marked this pull request as ready for review September 27, 2026 23:33
@alpertarhan
alpertarhan merged commit 6554063 into main Sep 27, 2026
2 checks passed
alpertarhan added a commit that referenced this pull request Sep 28, 2026
…ries; review-wave-1 fixes and the findings reports (#80)

* docs(findings): add the 2026-09-28 external review reports

Four audits of the 9.8.0-canary.7 wave (#75-#79), one folder per
model-harness pair, indexed from docs/README.md and docs/findings/README.md.
Advisory only; the package excludes docs/findings (package.json files).

* fix(hygiene): keep a carried trim through interrupted and busy boundaries; lifetime from the last cache writer

From the 2026-09-28 review reports (docs/findings), verified against the
code and Pi 0.87.1 before changing anything:

- turn_end dropped `applied` at its top and restored it only on a contested
  boundary. Pi persists an aborted/failed response with a fresh timestamp,
  so after such a turn the next request looked warm, went out untrimmed and
  rewrote the prefix the interrupted request had just cached (claude B1,
  deepseek P1-2, muse P0-2). `applied` now lives until a boundary commits
  it, unchangedSince fails, the session changes or contextHygieneEnabled is
  turned off.
- The cold check read the cache lifetime from the last response only; a
  fully cached or interrupted response after a 1h write made the prefix look
  5 minutes old (claude B2). cachedPrefix() dates the entry by the last
  response and takes the lifetime from the last response that wrote cache,
  the host-cache-ledger rule.
- canAutoTrim requires smart_context reachable (toolExposure.reachable
  "history"), as artifact offload does; ARCHITECTURE said so but nothing
  enforced it (deepseek P1-9, muse P0-5). Readiness names the inactive state.
- A queued change meeting an incomplete turn now leaves the "not applied"
  note (glm B2); committed rewinds reach the ledger as "rewind" (glm B1).
- noteForeignCompaction takes an explicit source instead of matching the
  notice text (claude B4); the before_switch/before_fork ledger resets were
  redundant with session_start (claude B5); markedAt was write-only.
- Comments: break-even N* counts the trimmed request (claude B7); the veto
  derivation states why the miss really costs missCost - w*X (deepseek P1-3
  and muse P0-3 are wrong: the warm path still reads X at r).
- Bench: price a held trim (trimTokens) beside plan trim (claude P1).
- package.json files excludes docs/findings (claude C2).

Tests: aborted/error turns append the interrupted response before turn_end;
busy boundary keeps the carried trim; cleanup off forgets it; 1h lifetime
survives a fully cached response; rewind attribution; incomplete-turn note.
alpertarhan added a commit that referenced this pull request Sep 28, 2026
Integration candidate for the maintainer's daily Pi (real-session data
collection on the cold-cache trim and the host cache ledger); not published.
CHANGELOG dates the [Unreleased] notes since canary.7 (#79-#83) under
9.8.0-canary.8; README and guide name the checkout version.

bun run release:check (env -i): typecheck clean; 1603 pass / 5 skip (Docker
sandbox) / 0 fail; adversarial gate 384/0; bench within limits; build;
release audit passed; compat:pi latest = 0.87.1.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant