From 763dd8a2a300b349be7ef6db2830b1d76149cbff Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Wed, 10 Jun 2026 07:46:30 -0400 Subject: [PATCH] fix(bake): strip model-conditional betas from baked base set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #485 made the drift CHECK ignore betaForModel()-managed betas, but the BAKE (capture-and-bake.mjs) still wrote the raw captured anthropic_beta verbatim — so a rebake from a capture riding context-1m (the runner's does) re-added it to the base, undoing #475 (seen in the closed #486). Run the scrubbed beta header through a shared stripModelConditionalBetas helper (drift-report.mjs, same MODEL_CONDITIONAL_BETAS set as the detector) before writing. Base never carries the per-request betas; betaForModel re-adds them at request time. test/bake-drift-report.mjs +2 (88/88). --- CHANGELOG.md | 2 ++ scripts/capture-and-bake.mjs | 13 ++++++++++++- scripts/drift-report.mjs | 16 ++++++++++++++++ test/bake-drift-report.mjs | 26 +++++++++++++++++++++++++- 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83c2850..4cb23f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ checklist. ## [Unreleased] +- **fix(bake): strip model-conditional betas from the baked base set (issue #484 follow-up).** The drift *detection* fix made `computeDrift` ignore the `betaForModel()`-managed betas, but the *bake* path (`capture-and-bake.mjs`) still wrote the raw captured `anthropic_beta` verbatim — so an auto-rebake from a capture that rode `context-1m-2025-08-07` (the drift runner's capture does) re-introduced it to the base, undoing #475. The bake now runs the captured beta header through `stripModelConditionalBetas` (new export, shares `MODEL_CONDITIONAL_BETAS`) before writing, so a rebake produces only the real diff (e.g. dropping `afk-mode-2026-01-31`) and never re-adds the per-request betas. `test/bake-drift-report.mjs` +2 cases (88/88). + - **fix(drift-check): stop the perpetual false-positive auto-rebake loop (issue #484).** `cc-drift-template-watch.yml` re-fired every cycle because `computeDrift` compared the live `--check` capture against the bundle without accounting for two structural, non-drift differences: (1) the **model-conditional betas** `context-1m-2025-08-07` and `fallback-credit-2026-06-01`, which `betaForModel()` appends per-request and the baked base deliberately omits — so a capture carrying them read as "beta added" forever; and (2) the **environment-specific memory directory path** in the system prompt, which differs on every cross-OS bake (e.g. a Windows-baked bundle vs the Linux drift runner's `/root/...` capture). `computeDrift` now filters the `betaForModel()`-managed betas from both sides (new export `MODEL_CONDITIONAL_BETAS`) and normalizes the memory path before the system_prompt diff (new export `normalizeMemoryPath`). Genuine base-beta changes (e.g. `afk-mode` add/removal) and real prompt edits still surface. `test/bake-drift-report.mjs` extended (+6 cases, 81/81). Closes the regression-rebake loop that produced #476 and #483. ## [4.8.55] - 2026-06-09 diff --git a/scripts/capture-and-bake.mjs b/scripts/capture-and-bake.mjs index ff9dcb2..34be4a2 100644 --- a/scripts/capture-and-bake.mjs +++ b/scripts/capture-and-bake.mjs @@ -41,7 +41,7 @@ import { fileURLToPath } from 'node:url'; import { captureLiveTemplateAsync, findInstalledCC } from '../dist/live-fingerprint.js'; import { scrubTemplate, findUserPathHits } from '../dist/scrub-template.js'; import { PLATFORM_ONLY_TOOLS } from '../dist/cc-template.js'; -import { computeDrift, formatDriftReport, interpretDrift, formatDriftSummary } from './drift-report.mjs'; +import { computeDrift, formatDriftReport, interpretDrift, formatDriftSummary, stripModelConditionalBetas } from './drift-report.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(__dirname, '..'); @@ -70,6 +70,17 @@ if (!captured) { log(`captured: CC v${captured._version}, ${captured.tools.length} tools, ${captured.system_prompt.length} char system prompt`); const scrubbed = scrubTemplate(captured); +// Strip the model-conditional betas betaForModel() appends per-request +// (context-1m on [1m] requests, fallback-credit on fable) so the baked BASE set +// matches #475's design — they're re-added per-request at runtime, not part of +// the canonical base. Without this, a capture that rode one (the drift runner's +// capture carries context-1m) would re-introduce it to the base on every rebake, +// undoing #475. Same set the drift detector ignores (drift-report.mjs). +const beforeBeta = scrubbed.anthropic_beta || ''; +scrubbed.anthropic_beta = stripModelConditionalBetas(beforeBeta); +if (scrubbed.anthropic_beta !== beforeBeta) { + log(`stripped model-conditional beta(s) from baked base: ${beforeBeta} → ${scrubbed.anthropic_beta}`); +} scrubbed._source = 'bundled'; scrubbed._supportedMaxTested = captured._version; diff --git a/scripts/drift-report.mjs b/scripts/drift-report.mjs index e3dd793..b9f09d9 100644 --- a/scripts/drift-report.mjs +++ b/scripts/drift-report.mjs @@ -20,6 +20,22 @@ export const MODEL_CONDITIONAL_BETAS = new Set([ 'fallback-credit-2026-06-01', // FABLE_FALLBACK_CREDIT_BETA — appended for fable requests only ]); +/** + * Strip the model-conditional betas (the ones betaForModel() appends per-request) + * from a comma-joined beta string. Used by the BAKE path (capture-and-bake.mjs) so + * the canonical base set written to cc-template-data.json never carries them — a + * live capture that happened to ride them (e.g. a `[1m]` request carrying + * context-1m) would otherwise re-introduce them to the base on every rebake, + * undoing #475's design. Mirrors the filter computeDrift applies for detection. + */ +export function stripModelConditionalBetas(beta) { + return (beta || '') + .split(',') + .filter(Boolean) + .filter((b) => !MODEL_CONDITIONAL_BETAS.has(b)) + .join(','); +} + /** * Collapse the environment-specific CC memory directory path to a placeholder so * a cross-OS bake doesn't read as system_prompt drift: the bundle may be baked on diff --git a/test/bake-drift-report.mjs b/test/bake-drift-report.mjs index 1e702a5..78694ce 100644 --- a/test/bake-drift-report.mjs +++ b/test/bake-drift-report.mjs @@ -3,7 +3,7 @@ // test runner spawns each file via node:test which is fine for imports // too, but the existing pattern groups script-imports in serial). -import { unifiedDiff, computeDrift, describeTool, formatDriftReport, interpretDrift, formatDriftSummary, MODEL_CONDITIONAL_BETAS, normalizeMemoryPath } from '../scripts/drift-report.mjs'; +import { unifiedDiff, computeDrift, describeTool, formatDriftReport, interpretDrift, formatDriftSummary, MODEL_CONDITIONAL_BETAS, normalizeMemoryPath, stripModelConditionalBetas } from '../scripts/drift-report.mjs'; let pass = 0; let fail = 0; @@ -389,6 +389,30 @@ header('37. computeDrift — real prompt edit still flagged despite path normali check('diff shows the real edit, not the path', d[0].detail?.some((l) => /CHANGED/.test(l)) && !d[0].detail?.some((l) => /\.claude/.test(l))); } +// ────────────────────────────────────────────────────────────────────── +// issue #484 — the BAKE must strip model-conditional betas so a rebake can't +// re-introduce them to the base (undoing #475). Mirrors the detection filter. +header('38. stripModelConditionalBetas — removes context-1m / fallback-credit, keeps the rest'); +{ + const captured = 'claude-code-20250219,context-1m-2025-08-07,interleaved-thinking-2025-05-14,effort-2025-11-24'; + const baked = stripModelConditionalBetas(captured); + check('context-1m removed', !baked.includes('context-1m-2025-08-07')); + check('base betas preserved in order', baked === 'claude-code-20250219,interleaved-thinking-2025-05-14,effort-2025-11-24'); + check('fallback-credit removed too', stripModelConditionalBetas('claude-code-20250219,fallback-credit-2026-06-01') === 'claude-code-20250219'); + check('no-op when no managed betas present', stripModelConditionalBetas('claude-code-20250219,afk-mode-2026-01-31') === 'claude-code-20250219,afk-mode-2026-01-31'); + check('empty / undefined safe', stripModelConditionalBetas('') === '' && stripModelConditionalBetas(undefined) === ''); +} + +header('39. bake-vs-check consistency — a re-baked base no longer drifts from the capture on managed betas'); +{ + // Simulate: live capture carries context-1m (rode a [1m] request); the bake + // strips it; computeDrift(baked-base, same-capture) must NOT re-flag it. + const capture = makeTemplate({ anthropic_beta: 'claude-code-20250219,context-1m-2025-08-07,effort-2025-11-24' }); + const baked = makeTemplate({ anthropic_beta: stripModelConditionalBetas(capture.anthropic_beta) }); + check('baked base omits context-1m', !baked.anthropic_beta.includes('context-1m')); + check('no beta drift between baked base and the capture it came from', computeDrift(baked, capture).length === 0); +} + // ────────────────────────────────────────────────────────────────────── console.log(`\n${pass} pass, ${fail} fail`); process.exit(fail === 0 ? 0 : 1);