From 995e642ae68851fdb47947758a2f0780c8f8a030 Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 07:53:21 +0800 Subject: [PATCH 1/7] fix(error-dump): count the #4482 banner inside the #4297 byte cap dumpRejectedBody/dumpSummaryRejection capped the body at the full cap and then prepended DUMP_PLAINTEXT_BANNER, so every dump file exceeded BILI_DUMP_4XX_MAX_BYTES by the banner length (~89B; shards failed with '0: 1111 bytes'). Reserve the banner bytes in both writers' budgets so the FINAL file honors the cap, and update the four #4297 assertions #4482 left stale (verbatim-no-banner expectation, direct JSON.parse of banner-prefixed files, exact-boundary sizes computed without the banner). Behavior change: dump files that previously overflowed the cap by up to one banner line now fit under it; bodies in (cap-banner, cap] are now truncated with the [truncated:] marker instead of overflowing. First-landing fix for the error-dump family listed in #4599's sentinel born-red cluster (not part of #4606). (cherry picked from commit 952d80b57e2a9446d113db48c976c75d0c9c8060) --- src/error-dump.ts | 8 ++++++-- tests/error-dump.test.ts | 14 +++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/error-dump.ts b/src/error-dump.ts index 116b03f2c..fd425e44c 100644 --- a/src/error-dump.ts +++ b/src/error-dump.ts @@ -57,11 +57,14 @@ export function dumpRejectedBody(status: number, sessionId: string, body: string if (!raw) return null; try { const cap = knobDump4xxMaxBytes(); + // #4482 banner rides ahead of the body β€” reserve its bytes so the FINAL + // file (banner included) honors the #4297 byte cap. + const budget = Math.max(0, cap - Buffer.byteLength(`${DUMP_PLAINTEXT_BANNER}\n`)); let text = raw; if (Buffer.byteLength(raw) <= cap) { try { text = JSON.stringify(JSON.parse(raw), null, 2); } catch { /* keep non-JSON bodies raw */ } } - text = capText(text, cap); + text = capText(text, budget); const dir = dumpsDir(); fs.mkdirSync(dir, { recursive: true }); const sid = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_"); @@ -97,7 +100,8 @@ export function dumpSummaryRejection(status: number, sessionId: string, request: const requestText = side(request); const responseText = side(response); const overhead = Buffer.byteLength(JSON.stringify({ status, request: "", response: "" }, null, 2)); - const budget = cap - overhead; + // Same #4482/#4297 reservation as dumpRejectedBody: final file including banner ≀ cap. + const budget = Math.max(0, cap - overhead - Buffer.byteLength(`${DUMP_PLAINTEXT_BANNER}\n`)); const requestBytes = textBytes(requestText, true, budget); const responseBytes = textBytes(responseText, true, budget); const requestBudget = Math.min(requestBytes, Math.max(budget - responseBytes, Math.floor(budget / 2))); diff --git a/tests/error-dump.test.ts b/tests/error-dump.test.ts index 34ae245a8..21eec2015 100644 --- a/tests/error-dump.test.ts +++ b/tests/error-dump.test.ts @@ -208,11 +208,14 @@ test("#4297 main dump enforces final UTF-8 bytes including markers and pretty pr test("#4297 main dump preserves bodies that fit exactly at the byte limit", () => { process.env.BILI_DUMP_4XX = "1"; process.env.BILI_DUMP_4XX_MAX_BYTES = "1024"; + // #4482: the banner rides ahead of the body and counts against the cap, so + // "fits" means fits within cap minus the banner line. + const budget = 1024 - Buffer.byteLength(`${DUMP_PLAINTEXT_BANNER}\n`); try { - for (const raw of ["x".repeat(1024), "汉".repeat(340) + "πŸ˜€", "small raw body"]) { + for (const raw of ["x".repeat(budget), "汉".repeat(Math.floor((budget - 4) / 3)) + "πŸ˜€", "small raw body"]) { const out = dumpRejectedBody(400, "boundary", raw); assert.ok(out); - assert.equal(fs.readFileSync(out, "utf8"), raw); + assert.equal(fs.readFileSync(out, "utf8"), `${DUMP_PLAINTEXT_BANNER}\n${raw}`); } } finally { restoreDumpEnv(); } }); @@ -232,7 +235,7 @@ test("#4297 summary cap covers both sides, JSON escaping and envelope bytes", () const out = dumpSummaryRejection(413, `summary-bytes-${i}`, request, response); assert.ok(out); assert.ok(fs.statSync(out).size <= 1024, `${i}: ${fs.statSync(out).size} bytes`); - const parsed = JSON.parse(fs.readFileSync(out, "utf8")) as { status: number; request: string; response: string }; + const parsed = JSON.parse(stripDumpBanner(fs.readFileSync(out, "utf8"))) as { status: number; request: string; response: string }; assert.equal(parsed.status, 413); assert.equal(typeof parsed.request, "string"); assert.equal(typeof parsed.response, "string"); @@ -248,12 +251,13 @@ test("#4297 summary exact byte boundary stays complete; one more byte truncates process.env.BILI_DUMP_4XX = "1"; process.env.BILI_DUMP_4XX_MAX_BYTES = "1024"; const overhead = Buffer.byteLength(JSON.stringify({ status: 400, request: "", response: "" }, null, 2)); - const exact = "x".repeat(1024 - overhead); + const budget = 1024 - overhead - Buffer.byteLength(`${DUMP_PLAINTEXT_BANNER}\n`); + const exact = "x".repeat(budget); try { for (const request of [exact, exact + "x"]) { const out = dumpSummaryRejection(400, "summary-boundary", request, ""); assert.ok(out); - const parsed = JSON.parse(fs.readFileSync(out, "utf8")) as { request: string; response: string }; + const parsed = JSON.parse(stripDumpBanner(fs.readFileSync(out, "utf8"))) as { request: string; response: string }; assert.ok(fs.statSync(out).size <= 1024); assert.equal(parsed.response, ""); if (request === exact) assert.equal(parsed.request, exact); From 6bf59b9640f9fef3dfdf85e7fd2b3c25d92fd5ba Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 10:22:57 +0800 Subject: [PATCH 2/7] test: pin the #3013 refusal-wording assertions to the wording that actually shipped (#4599 sentinel) The three regexes never matched: /terminal for the session/ vs shipped 'unconditional and terminal (bili_final)', /allowDshCompaction/ absent from the storm log (its levers are #2474 + 'Disable dsh auto-compaction'), and /terminal; #3013/ vs shipped '(terminal, bili_final; #3013)'. The source wording is unchanged since the test's own birth commit 3d39ba601, so the file was red from birth. Each assertion keeps its semantic teeth. --- tests/issue3013-refusal-terminal.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/issue3013-refusal-terminal.test.ts b/tests/issue3013-refusal-terminal.test.ts index 0d60e52f5..142059b40 100644 --- a/tests/issue3013-refusal-terminal.test.ts +++ b/tests/issue3013-refusal-terminal.test.ts @@ -92,7 +92,7 @@ test("A1: refusal bodies carry the bili_final terminal marker on both protocol s assert.equal(err.code, "dsh_compaction_refused"); // the message must state the terminal semantics + the levers (#3013) - assert.match(String(err.message), /terminal for the session/); + assert.match(String(err.message), /unconditional and terminal \(bili_final\)/); assert.match(String(err.message), /stop retrying/); assert.match(String(err.message), /#2474/); assert.match(String(err.message), /allowDshCompaction/); @@ -131,7 +131,7 @@ test("A3: pipeline storm β€” first refusal warns once, then every DSH_REFUSAL_ST assert.match(storm[0].msg, new RegExp(`refusal #${DSH_REFUSAL_STORM_LOG_EVERY}\\b`)); assert.match(storm[0].msg, /never shrinks \(#2490\)/); assert.match(storm[0].msg, /#2474/); - assert.match(storm[0].msg, /allowDshCompaction/); + assert.match(storm[0].msg, /Disable dsh auto-compaction/); assert.match(storm[0].msg, /#3013/); } finally { setLogCapture(null); @@ -172,7 +172,7 @@ test("A3: passthrough storm β€” first-in-process warns, then the same cadence (n if (firstInLoop) { assert.equal(warns.length, 1, "process-first refusal warns"); assert.equal(infos.length, 1, "and the 100th logs one cadence info"); - assert.match(infos[0].msg, /terminal; #3013/); + assert.match(infos[0].msg, /terminal, bili_final; #3013/); } else { assert.equal(infos.length, 1, "exactly one cadence info per 100 consecutive refusals"); } From 6843789d82a135f16ffdd11d86cb07b716e8c85f Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 10:22:57 +0800 Subject: [PATCH 3/7] test: re-arm the cancelled growth auto-fold fixture past the #3444 benefit gate (#4599 sentinel) 30ae2188a added AUTO_FOLD_MIN_BENEFIT_TOKENS=4096 (src/external-summary-settings.ts:99); the fixture's 10_000 baseline vs 8192 target left a 1808-token margin, so triggerFires silently went false and preflight returned before any log line (f.logs == []). Raising the measured baseline to 20_000 (margin 11_808) re-arms the growth fold the test exists to cancel mid-flight. --- tests/preflight-client-abort.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/preflight-client-abort.test.ts b/tests/preflight-client-abort.test.ts index 3557253d1..759641fb9 100644 --- a/tests/preflight-client-abort.test.ts +++ b/tests/preflight-client-abort.test.ts @@ -175,14 +175,17 @@ for (const pluginMode of [false, true]) { test("cancelled growth auto-fold stops before fail-open forwarding and backoff", async () => { await withUpstream(async (url, mainCalls) => { const f = fixture(url, 50_000); - f.session.stats.lastInputTokens = 10_000; + // #3444 benefit gate: the growth arm needs decisionTrigger βˆ’ target β‰₯ + // AUTO_FOLD_MIN_BENEFIT_TOKENS (4096) β€” 10_000 vs 8192 left only 1808, + // so the trigger silently disarmed after 30ae2188a. 20_000 re-arms it. + f.session.stats.lastInputTokens = 20_000; f.session.stats.lastInputTokensSource = "usage"; Object.assign(f.config, { externalSummary: parseExternalSummarySettings({ enabled: true, autoFold: true, autoFoldTargetTokens: 8192, targets: [{ name: "sum", protocol: "responses", url, model: "test-model", apiKey: "fixture-key" }], }, { inlineKeys: true }) }); f.prepared.nudge = f.core.processTurn({ messages: f.messages, state: f.session.state, - config: f.config, tokenCount: 10_000, renderTags: "text-only" }).nudge; + config: f.config, tokenCount: 20_000, renderTags: "text-only" }).nudge; assert.ok(f.prepared.nudge?.compressibleRanges.length); const processTurn = f.core.processTurn.bind(f.core); f.core.processTurn = params => { From dc86ca8398bce6c7b3d88f4d0697115245343cda Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 10:49:17 +0800 Subject: [PATCH 4/7] fix(reconcile): latch the epoch-confirm pause only when covered ids still survive (#4260 born-red, #4599 sentinel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both failures were born red at 594df7236/a5d1994d1 (the #4260 fix itself, Oct 22) and untouched since β€” fold-reconcile.ts escalation region is byte-identical from that commit to master. - tests/epoch-confirm-lifecycle.test.ts 'all-missing' shape: its explicit per-shape spec says zero survivors => never latch foldEpochConfirmPending (nothing to align onto or offer back; latching would freeze folding forever over what may be a brand-new conversation on a reused session id). The implementation latched unconditionally. Now the PAUSE latches only when some covered id survives (present or claimed, anyCoveredAligned); the ONE error line per episode (#2193/#2297), the ESCALATED latch, the witness event and snapshot retention are unchanged. - tests/issue2915-order-centric-reconcile.test.ts post-escalation section still asserted the SUPERSEDED pre-#4260 contract (escalation reaps => clean slate => 'noop'). Under #4260 retention the block survives: healthy resend is a fully clean 'resend' pass, the retained block stays (length 1), and an id re-mint re-anchors positionally ('reanchored'). - MESSAGE-IDENTITY.md epoch-confirm bullet synced with the carve-out. --- MESSAGE-IDENTITY.md | 12 ++++++---- src/fold-reconcile.ts | 15 +++++++++++- .../issue2915-order-centric-reconcile.test.ts | 24 ++++++++++--------- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/MESSAGE-IDENTITY.md b/MESSAGE-IDENTITY.md index 2cef13751..4bdb5416d 100644 --- a/MESSAGE-IDENTITY.md +++ b/MESSAGE-IDENTITY.md @@ -214,10 +214,14 @@ rule; proof tests in `tests/fold-anchor-doctrine.test.ts`): frozen (`foldEpisodeOrder`); a foreign conversation under the same session id has no continuity with it, so wholesale replacement cannot masquerade as truncation. -- **Epoch-confirm gate**: total collapse latches `foldEpochConfirmPending`, - emits one error, and preflight passes requests through verbatim β€” folding - *pauses*; nothing is deleted, nothing is silently reset. Alignment - recovery (the host resends the true history) auto-clears the latch. +- **Epoch-confirm gate**: total collapse emits one error and latches + `foldEpochConfirmPending` β€” preflight passes requests through verbatim, + folding *pauses*; nothing is deleted, nothing is silently reset. The pause + latches only when some covered id still survives on the wire (present or + claimed): with zero survivors there is nothing to confirm against (#4260 + regression spec), so folding keeps running and the retained snapshot rides + the ordinary zombie lifecycle. Alignment recovery (the host resends the + true history) auto-clears the latch. - **Unresolved-anchor placement** (kernel `prune.ts`): a block whose anchors all failed to resolve now inserts at the last user message instead of the tail β€” a summary block can no longer ride *after* the user's newest diff --git a/src/fold-reconcile.ts b/src/fold-reconcile.ts index ea16e6c83..b0440450d 100644 --- a/src/fold-reconcile.ts +++ b/src/fold-reconcile.ts @@ -1960,6 +1960,10 @@ export function reconcileFoldCoverage(session: Session, msgs: CoreMessage[], opt const prevCov = (session.metadata[METADATA_FOLD_COVERAGE] as Record | undefined) ?? {}; const nextCov: Record = {}; let covCount = 0; + // #4260 regression spec: alignment evidence = some covered id still on the + // wire (present) or claimed (re-anchored). Consumed by the epoch-confirm + // latch below: with zero survivors there is nothing to confirm against. + let anyCoveredAligned = false; for (const block of blocks) { const bid = block.blockId; if (bid === undefined || bid === "") continue; @@ -1974,6 +1978,7 @@ export function reconcileFoldCoverage(session: Session, msgs: CoreMessage[], opt else if (plan.claims.has(id)) r++; } const rec: FoldBlockCoverage = { p, r, t: ids.length }; + if (p + r > 0) anyCoveredAligned = true; if (prevCov[bid]?.e === 1 || p + r > 0) rec.e = 1; // #2695: zombie streak β€” strict majority of covered ids absent, only // for VERIFIED substrates (e === 1). Never-represent substrates keep @@ -2148,7 +2153,15 @@ export function reconcileFoldCoverage(session: Session, msgs: CoreMessage[], opt // the user once: re-anchor onto the new history (new epoch) or // restore the old one. Either alignment event clears the latch // automatically (resetFoldDriftState on the first clean pass). - session.metadata[METADATA_EPOCH_CONFIRM] = true; + // #4260 regression spec: the PAUSE latches only when some covered + // id still survives (present or claimed) β€” with zero survivors + // there is nothing to align onto or offer back, and latching would + // freeze folding forever over what may be a brand-new conversation + // on a reused session id. The episode still gets its ONE error line + // (#2193/#2297), the ESCALATED latch, the witness event, and the + // snapshot stays retained (epochProtected above); the ordinary + // zombie lifecycle then decides the dead span's fate. + if (anyCoveredAligned) session.metadata[METADATA_EPOCH_CONFIRM] = true; if (opts.log !== undefined) { opts.log("error", `${tag}[fold-reconcile] fold epoch confirmation required: ${plan.unmatched.length} covered id(s) missing with NO anchor match and NO word evidence for ${streak} consecutive passes${span} β€” the resent history shares nothing with the fold substrate (wholesale replacement: host-native compaction outside bili's knowledge #1729/#2193, or a brand-new conversation on the same session id). Folding is paused for this session until the histories realign (old history resent or a fresh fold on the new one starts a new epoch); bili keeps the existing snapshot β€” nothing is deleted (#3641)${driftTok}`); } diff --git a/tests/issue2915-order-centric-reconcile.test.ts b/tests/issue2915-order-centric-reconcile.test.ts index ce74154a5..46c24a54e 100644 --- a/tests/issue2915-order-centric-reconcile.test.ts +++ b/tests/issue2915-order-centric-reconcile.test.ts @@ -360,21 +360,23 @@ describe("escalation latch recovery (review gap β‘‘) β€” upgrade-in-place heals" assert.equal(logs.filter((l) => l.msg.includes("fold epoch confirmation required")).length, 1, "#2297: exactly ONE error line per episode"); - // healthy resend of the ORIGINAL bytes: missing=0 β†’ noop, and the - // streak reset clears the latch β€” the upgrade-in-place recovery path. + // healthy resend of the ORIGINAL bytes: everything present β†’ fully + // clean pass ("resend"), and the streak reset clears the latch β€” the + // upgrade-in-place recovery path. const healthy = reconcileFoldCoverage(session, originals, opts(log)); - assert.equal(healthy.kind, "noop"); + assert.equal(healthy.kind, "resend"); assert.ok(session.metadata.foldDriftEscalated === undefined, "latch cleared on recovery"); assert.ok(session.metadata.foldDriftStreak === undefined, "streak cleared on recovery"); - // Escalation reaped the destroyed block (#2695 zombie-carrier reap) β€” - // so after the latch clears the ledger is a CLEAN SLATE, not a repair - // target: nothing is covered, nothing missing, and the next fold - // cycle starts fresh. That is the upgrade-in-place contract: a - // v0.1.192β†’v0.1.196 upgrade with a latched ledger heals instead of - // dead-looping the doomed preflight. - assert.equal((session.state.blocks as unknown[]).length, 0, "destroyed substrate reaped at escalation"); + // #4260 superseded the #2695 clean-slate contract: escalation no + // longer reaps β€” the fold snapshot is RETAINED while the ledger heals + // (nothing is deleted, nothing is silently reset), so after the latch + // clears the session keeps its block and the next id churn re-anchors + // against it instead of starting from scratch. That is the + // upgrade-in-place contract: a v0.1.192β†’v0.1.196 upgrade with a + // latched ledger heals instead of dead-looping the doomed preflight. + assert.equal((session.state.blocks as unknown[]).length, 1, "#4260: destroyed substrate retained while epoch confirmation pending"); const r = reconcileFoldCoverage(session, remint(originals, "g9"), opts(log)); - assert.equal(r.kind, "noop", "clean slate β€” the lane lives, folds rebuild on the next compress cycle"); + assert.equal(r.kind, "reanchored", "retained snapshot re-anchors the id re-mint positionally"); }); }); From b06b408629f2f3ef27ede600e97e842e56b6446a Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 11:03:50 +0800 Subject: [PATCH 5/7] fix(reconcile): keep the #3641 epoch-confirm latch unconditional; align the #4260 test to it instead (#4599 sentinel, supersedes dc86ca839's carve-out) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dc86ca839 gated the foldEpochConfirmPending latch on 'some covered id still survives' to satisfy tests/epoch-confirm-lifecycle.test.ts's all-missing shape. That broke two previously-green suites pinning the OPPOSITE contract for the identical input class (zero survivors, >=10 unmatched, proxy mode): tests/fold-anchor-doctrine.test.ts '#3641 total collapse asks for epoch confirmation' (:213) + 'epoch-confirm suspends preflight compression', and tests/fold-reconcile.test.ts 'side-request passes ... reset nothing'. No implementation can satisfy both β€” a genuine spec conflict between the #3641 doctrine and the #4260 born-red test. Resolution: the #3641 side wins. It is the documented doctrine (MESSAGE-IDENTITY.md epoch-confirm bullet), it has been green-enforcing for days, and its preflight-gate test shows why the pause matters most exactly in the zero-survivor case: the substrate is maximally unprovable, the ask is which history owns the session going forward, and folding must not silently start a new epoch on foreign bytes ('nothing is silently reset'). So: - src/fold-reconcile.ts + MESSAGE-IDENTITY.md: reverted to master state (latch unconditional again β€” zero net production behavior change vs master from this family). - tests/epoch-confirm-lifecycle.test.ts all-missing shape: expectation aligned to the doctrine (pending latches at pass >= 3 like the survivor shapes); retention assertions (block kept, deactivated) unchanged. - The dc86ca839 update of tests/issue2915-order-centric-reconcile.test.ts (superseded #2695 reap contract -> #4260 retention: resend / block kept / reanchored) STANDS β€” it is independent of the latch question and was red at birth under master code too. Verified locally: epoch-confirm-lifecycle 3/3, issue2915 5/5, fold-anchor-doctrine 7/7, fold-reconcile 13/13, typecheck clean. --- MESSAGE-IDENTITY.md | 12 ++++-------- src/fold-reconcile.ts | 15 +-------------- tests/epoch-confirm-lifecycle.test.ts | 9 ++++++++- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/MESSAGE-IDENTITY.md b/MESSAGE-IDENTITY.md index 4bdb5416d..2cef13751 100644 --- a/MESSAGE-IDENTITY.md +++ b/MESSAGE-IDENTITY.md @@ -214,14 +214,10 @@ rule; proof tests in `tests/fold-anchor-doctrine.test.ts`): frozen (`foldEpisodeOrder`); a foreign conversation under the same session id has no continuity with it, so wholesale replacement cannot masquerade as truncation. -- **Epoch-confirm gate**: total collapse emits one error and latches - `foldEpochConfirmPending` β€” preflight passes requests through verbatim, - folding *pauses*; nothing is deleted, nothing is silently reset. The pause - latches only when some covered id still survives on the wire (present or - claimed): with zero survivors there is nothing to confirm against (#4260 - regression spec), so folding keeps running and the retained snapshot rides - the ordinary zombie lifecycle. Alignment recovery (the host resends the - true history) auto-clears the latch. +- **Epoch-confirm gate**: total collapse latches `foldEpochConfirmPending`, + emits one error, and preflight passes requests through verbatim β€” folding + *pauses*; nothing is deleted, nothing is silently reset. Alignment + recovery (the host resends the true history) auto-clears the latch. - **Unresolved-anchor placement** (kernel `prune.ts`): a block whose anchors all failed to resolve now inserts at the last user message instead of the tail β€” a summary block can no longer ride *after* the user's newest diff --git a/src/fold-reconcile.ts b/src/fold-reconcile.ts index b0440450d..ea16e6c83 100644 --- a/src/fold-reconcile.ts +++ b/src/fold-reconcile.ts @@ -1960,10 +1960,6 @@ export function reconcileFoldCoverage(session: Session, msgs: CoreMessage[], opt const prevCov = (session.metadata[METADATA_FOLD_COVERAGE] as Record | undefined) ?? {}; const nextCov: Record = {}; let covCount = 0; - // #4260 regression spec: alignment evidence = some covered id still on the - // wire (present) or claimed (re-anchored). Consumed by the epoch-confirm - // latch below: with zero survivors there is nothing to confirm against. - let anyCoveredAligned = false; for (const block of blocks) { const bid = block.blockId; if (bid === undefined || bid === "") continue; @@ -1978,7 +1974,6 @@ export function reconcileFoldCoverage(session: Session, msgs: CoreMessage[], opt else if (plan.claims.has(id)) r++; } const rec: FoldBlockCoverage = { p, r, t: ids.length }; - if (p + r > 0) anyCoveredAligned = true; if (prevCov[bid]?.e === 1 || p + r > 0) rec.e = 1; // #2695: zombie streak β€” strict majority of covered ids absent, only // for VERIFIED substrates (e === 1). Never-represent substrates keep @@ -2153,15 +2148,7 @@ export function reconcileFoldCoverage(session: Session, msgs: CoreMessage[], opt // the user once: re-anchor onto the new history (new epoch) or // restore the old one. Either alignment event clears the latch // automatically (resetFoldDriftState on the first clean pass). - // #4260 regression spec: the PAUSE latches only when some covered - // id still survives (present or claimed) β€” with zero survivors - // there is nothing to align onto or offer back, and latching would - // freeze folding forever over what may be a brand-new conversation - // on a reused session id. The episode still gets its ONE error line - // (#2193/#2297), the ESCALATED latch, the witness event, and the - // snapshot stays retained (epochProtected above); the ordinary - // zombie lifecycle then decides the dead span's fate. - if (anyCoveredAligned) session.metadata[METADATA_EPOCH_CONFIRM] = true; + session.metadata[METADATA_EPOCH_CONFIRM] = true; if (opts.log !== undefined) { opts.log("error", `${tag}[fold-reconcile] fold epoch confirmation required: ${plan.unmatched.length} covered id(s) missing with NO anchor match and NO word evidence for ${streak} consecutive passes${span} β€” the resent history shares nothing with the fold substrate (wholesale replacement: host-native compaction outside bili's knowledge #1729/#2193, or a brand-new conversation on the same session id). Folding is paused for this session until the histories realign (old history resent or a fresh fold on the new one starts a new epoch); bili keeps the existing snapshot β€” nothing is deleted (#3641)${driftTok}`); } diff --git a/tests/epoch-confirm-lifecycle.test.ts b/tests/epoch-confirm-lifecycle.test.ts index 071571beb..61388ac3d 100644 --- a/tests/epoch-confirm-lifecycle.test.ts +++ b/tests/epoch-confirm-lifecycle.test.ts @@ -58,7 +58,14 @@ for (const shape of ["one-interior-survivor", "all-missing", "small-zombie"] as if (shape === "all-missing") { assert.equal(session.state.blocks.length, 1); assert.equal(session.state.blocks[0].active, false); - assert.equal(session.metadata.foldEpochConfirmPending === true, false); + // #3641 doctrine: zero survivors latches the pause too β€” the + // substrate is maximally unprovable and the ask is which + // history owns the session going forward; folding must not + // silently start a new epoch on foreign bytes. The original + // `false` here contradicted the #3641 doctrine suites + // (fold-anchor-doctrine) and was born-red at #4260; aligned + // to the doctrine instead of carving the implementation out. + assert.equal(session.metadata.foldEpochConfirmPending === true, pass >= 3); } } assert.equal(session.blockContents.get(blockId)?.full.text, original); From 6ee46e018a997180a4d5bd4009c8436b78716a55 Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 13:57:47 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix(ci):=20restamp=20dist=20mtimes=20on=20v?= =?UTF-8?q?erified=20pretest-cache=20HIT=20(#4360=C3=97#3325,=20#4599=20se?= =?UTF-8?q?ntinel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/cache restores a verified-fresh kernel/pi-subagents dist with SAVE-TIME mtimes, older than the just-checked-out sources; tests/golden-storage-drive.ts' #3325 staleness guard is a pure mtime oracle (newest kernel/src vs node_modules/acp-kernel/dist/index.js), so every cache-hit leg reds every golden drive ~4ms in ('dist is older than kernel/src'). The marker already proved content freshness β€” restamp the verified dist tree's mtimes to now so the two oracles agree. Content untouched; HIT still never rebuilds. Regression test pins the restore-then-warm sequence end to end. --- scripts/ci-pretest-cache.mjs | 31 ++++++++++++++++++++++-- tests/ci-pretest-cache.test.ts | 43 +++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/scripts/ci-pretest-cache.mjs b/scripts/ci-pretest-cache.mjs index 33b155d76..3064fe643 100644 --- a/scripts/ci-pretest-cache.mjs +++ b/scripts/ci-pretest-cache.mjs @@ -23,7 +23,14 @@ // // The workflow side (actions/cache keyed on hashFiles over the SAME inputs, // per runner.os) only accelerates the MISS path across legs; the marker -// check stands on its own, so local `npm test` warm runs benefit too. +// check stands on its own, so local `npm test` warm runs benefit too. One +// side effect it must undo: a restore hands the dist back with SAVE-TIME +// mtimes, older than the just-checked-out sources. tests/golden-storage-drive.ts' +// #3325 staleness guard is a pure MTIME oracle (newest kernel/src vs +// node_modules/acp-kernel/dist/index.js), so the HIT path restamps the +// verified dist tree's mtimes to now β€” content untouched, the two oracles +// agree again (#4599 sentinel: without this, every cache-hit leg reds the +// storage-render golden chat wire ~4ms into the drive). // // Parity note: pretest previously ran `npm --prefix kernel run build && npm // --prefix pi-subagents run build`; this script runs exactly those commands @@ -32,7 +39,7 @@ import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, utimesSync, writeFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -128,6 +135,25 @@ export function markerIsFresh(distDir, hash) { } } +/** + * Mtime reconciliation after a VERIFIED hit (#4360Γ—#3325): the marker proved + * the bytes are current, but a cache restore left save-time mtimes behind + * the checked-out sources. Restamp every file's mtime to now so any + * mtime-based freshness oracle over the dist agrees with the content oracle. + * Content is never touched. + */ +function restampMtimes(distDir) { + const now = new Date(); + const walk = (dir) => { + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (e.isFile()) utimesSync(p, now, now); + } + }; + walk(distDir); +} + /** * The pretest loop. For each component: hash β†’ fresh marker? HIT, skip : * MISS β†’ run the real build β†’ stamp the marker. `build`/`log` are test seams @@ -144,6 +170,7 @@ export function runPretestCache(opts = {}) { const hash = hashComponent(root, comp); const distDir = join(root, comp.dir, "dist"); if (markerIsFresh(distDir, hash)) { + restampMtimes(distDir); log(`pretest-cache: HIT (${comp.name}) β€” dist fresh for hash ${hash.slice(0, 12)}`); results.push({ name: comp.name, hit: true, ms: 0 }); continue; diff --git a/tests/ci-pretest-cache.test.ts b/tests/ci-pretest-cache.test.ts index cb76d9d91..871ac14c2 100644 --- a/tests/ci-pretest-cache.test.ts +++ b/tests/ci-pretest-cache.test.ts @@ -1,6 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import * as cache from "../scripts/ci-pretest-cache.mjs"; @@ -89,6 +89,47 @@ test("cold β†’ warm β†’ tamper lifecycle (the #4360 CI sequence)", () => { assert.ok(lines.some((l) => l.startsWith("pretest-cache: MISS (kernel) β†’ rebuilding"))); }); +// #4599 sentinel (diagnosed on run 40161560956): actions/cache restores a +// verified-fresh dist with SAVE-TIME mtimes while the fresh checkout leaves +// the sources newer β€” content byte-identical, clock says stale. The #3325 +// guard in tests/golden-storage-drive.ts is a pure mtime oracle and reds +// every cache-hit leg's golden drive ~4ms in. A verified HIT must reconcile +// the two oracles by restamping dist mtimes, never by rebuilding or by +// touching bytes. +test("verified HIT restamps dist mtimes ahead of the checked-out sources (#4360Γ—#3325)", () => { + const { root } = makeMiniRepo(); + const log = (): void => {}; + // COLD: real build materializes dist + marker. + cache.runPretestCache({ root, build: fakeBuild([]), log }); + + // Simulate the CI sequence: restore hands the dist back 24h old, the + // checkout leaves every source at now. + const old = new Date(Date.now() - 24 * 60 * 60 * 1000); + utimesSync(join(root, "kernel/dist/index.js"), old, old); + for (const rel of ["kernel/src/index.ts", "kernel/src/deep/util.ts", "kernel/tsup.config.ts"]) { + utimesSync(join(root, rel), new Date(), new Date()); + } + const srcAt = Math.max( + statSync(join(root, "kernel/src/index.ts")).mtimeMs, + statSync(join(root, "kernel/src/deep/util.ts")).mtimeMs, + statSync(join(root, "kernel/tsup.config.ts")).mtimeMs, + ); + const distBefore = statSync(join(root, "kernel/dist/index.js")).mtimeMs; + assert.ok(srcAt > distBefore, "precondition: restored dist sits behind the checked-out sources"); + + // WARM: marker still byte-exact β†’ HIT, zero builds… + const seen: string[] = []; + const r = cache.runPretestCache({ root, build: fakeBuild(seen), log }); + assert.deepEqual(r.map((x) => x.hit), [true, true]); + assert.equal(seen.length, 0); + + // …and afterwards the #3325 mtime condition can no longer fire, with + // the dist bytes untouched. + const distAfter = statSync(join(root, "kernel/dist/index.js")).mtimeMs; + assert.ok(distAfter >= srcAt, "HIT must restamp the dist ahead of the checked-out sources"); + assert.equal(readFileSync(join(root, "kernel/dist/index.js"), "utf-8"), "built-bytes"); +}); + test("marker must match byte-exactly β€” absent, stale, tampered, CRLF are decided by content", () => { const { root } = makeMiniRepo(); const kernel = cache.COMPONENTS[0]; From 12a21087445bafe0c27bcd2065cfa4152418b5e3 Mon Sep 17 00:00:00 2001 From: ci-check Date: Sun, 25 Oct 2026 13:57:47 +0800 Subject: [PATCH 7/7] test: release store handles before teardown rmSync in #4316 PR3 suite (windows EBUSY, #4599 sentinel) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S12a/S12b opened UnifiedStore handles that were never closed; test.after's rmSync(testRoot) then fails on Windows with EBUSY (resource busy or locked, unlink ...\index.db) because Windows cannot unlink an open SQLite file β€” born-red on every windows leg since the suite landed. Close each handle at the end of its test (same discipline as plugin-fork's closeQuietly). --- tests/seq-ledger-pr3.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/seq-ledger-pr3.test.ts b/tests/seq-ledger-pr3.test.ts index 78e382279..eb6c74317 100644 --- a/tests/seq-ledger-pr3.test.ts +++ b/tests/seq-ledger-pr3.test.ts @@ -98,6 +98,7 @@ test("S12a: cloneSessionRows copies parent rows with identical seqs, lineage col assert.equal(after.length, 7); assert.equal(after[6]!.seq, 6, "child append lands at parent max seq + 1"); assert.equal(after[6]!.msgId, "g"); + st.close(); // release the handle before test.after's rmSync β€” Windows cannot unlink an open index.db }); test("S12b: partial branch (throughSeq) clones only the prefix", async () => { @@ -108,6 +109,7 @@ test("S12b: partial branch (throughSeq) clones only the prefix", async () => { const rows = st.getMessages("pr3-child-2"); assert.equal(rows.length, 3); assert.deepEqual(rows.map((r) => r.msgId), ["a", "b", "c"]); + st.close(); // release the handle before test.after's rmSync β€” Windows cannot unlink an open index.db }); test("S14: superseded-ref compress rejection β€” old refs refused with guidance, current refs pass", () => {