Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions src/cache-ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { METADATA_CANONICAL_SYS_FP, METADATA_FOLD_COVERAGE, METADATA_LAST_RESPON
import { clearPostRebuildAnchor, markDirty, type Session } from "./session.js";
import { isForeignMembership } from "./request-membership.js";
import { billInputUsage } from "./billing.js";
import { normalizeUpstreamOrigin } from "./util.js";
import { CALIBRATION_CLAMP_MAX, CALIBRATION_CLAMP_MIN, normalizeUpstreamOrigin } from "./util.js";
import { toolFail, toolOk, type ProxyToolResult } from "./proxy-tool-result.js";

// Render window for handleAcpCache's detail:"full" text view (#1489). The
Expand Down Expand Up @@ -1716,9 +1716,10 @@ export const CALIBRATION_SAMPLE_MAX = 5;
// it covers the measured CJK band and stays BELOW CALIBRATION_SAMPLE_MAX (5),
// so non-corresponding reports are still rejected before the clamp sees them.
// Wild values remain bounded by the evidence gates: ≥2 recent samples agreeing
// within ×2, keyed by route AND model, MIN_ESTIMATE floor.
export const CALIBRATION_CLAMP_MIN = 0.25;
export const CALIBRATION_CLAMP_MAX = 4;
// within ×2, keyed by route AND model, MIN_ESTIMATE floor. Since #5373 the
// constants live in util.ts (the read side needs them too) and are
// re-exported here so existing imports keep resolving.
export { CALIBRATION_CLAMP_MAX, CALIBRATION_CLAMP_MIN } from "./util.js";
// Evidence requirements: ≥2 recent same-route samples agreeing within ×2.
// One lucky/degenerate pair must not flip every estimate on the route.
export const CALIBRATION_SAMPLE_WINDOW = 3;
Expand Down Expand Up @@ -1755,7 +1756,16 @@ function ingestCalibrationPair(session: Session, billed: number, upstream: strin
const calibPolicy = pendingCalibOrigin === settleOrigin ? session.stats.lastCalibrationPolicy : undefined;
const pendingEst = session.stats.lastLocalTextEstimate;
const pendingOrigin = normalizeUpstreamOrigin(session.stats.lastLocalTextEstimateOrigin);
if (pendingEst !== undefined && pendingEst >= CALIBRATION_MIN_ESTIMATE && settleOrigin !== undefined && pendingOrigin === settleOrigin) {
if (pendingEst === undefined || pendingEst < CALIBRATION_MIN_ESTIMATE || settleOrigin === undefined || pendingOrigin !== settleOrigin) {
// #5373: this gate used to fail SILENTLY — in production there was no
// trace whether a measurement was ever ingested, which is exactly what
// made the #5373 incident undiagnosable from the log. One debug line
// with the reason keeps healthy sessions quiet but makes every miss
// greppable.
loggerLog("debug", `[${session.id}] calibration sample not ingested: ${pendingEst === undefined ? "no pending local estimate" : pendingEst < CALIBRATION_MIN_ESTIMATE ? `local estimate ${Math.round(pendingEst)} below min ${CALIBRATION_MIN_ESTIMATE}` : settleOrigin === undefined ? "no settle origin" : `origin mismatch (${pendingOrigin ?? "?"} ≠ ${settleOrigin})`}, billed=${billed} (#1933)`);
return;
}
{
const rawSample = billed / pendingEst;
// #2117 B: the ring is keyed by route AND model — tokenizers bill
// differently across models, so a mid-session model switch starts a
Expand All @@ -1777,6 +1787,7 @@ function ingestCalibrationPair(session: Session, billed: number, upstream: strin
if (rawSample >= CALIBRATION_SAMPLE_MIN && rawSample <= CALIBRATION_SAMPLE_MAX) {
ring.values.push(rawSample);
if (ring.values.length > CALIBRATION_SAMPLE_WINDOW) ring.values.shift();
loggerLog("debug", `[${session.id}] calibration sample ${rawSample.toFixed(3)} ingested (billed ${billed} / local est ${Math.round(pendingEst)}) — ring [${ring.values.map((v) => v.toFixed(3)).join(", ")}] route=${settleOrigin}${settleModel ? ` model=${settleModel}` : ""} (#1933)`);
const spread = Math.max(...ring.values) / Math.min(...ring.values);
if (ring.values.length >= 2 && spread <= CALIBRATION_CONSISTENCY_RATIO) {
const mean = ring.values.reduce((a, b) => a + b, 0) / ring.values.length;
Expand All @@ -1786,6 +1797,7 @@ function ingestCalibrationPair(session: Session, billed: number, upstream: strin
delete session.stats.calibratedEstimate;
delete session.stats.calibratedEstimateOrigin;
delete session.stats.calibratedEstimateModel;
loggerLog("info", `[${session.id}] calibration k̂ NOT published — route policy disabled (providers[<key>].calibration.disabled) (#3211)`);
} else {
const clamped = Math.min(CALIBRATION_CLAMP_MAX, Math.max(CALIBRATION_CLAMP_MIN, mean));
session.stats.calibratedEstimate = calibPolicy?.max !== undefined ? Math.min(clamped, calibPolicy.max) : clamped;
Expand All @@ -1796,12 +1808,17 @@ function ingestCalibrationPair(session: Session, billed: number, upstream: strin
// branch: the disabled path above clears all three fields and
// must not re-stamp one of them (#3211 review).
session.stats.calibratedEstimateModel = ring.model;
loggerLog("info", `[${session.id}] calibration k̂=${session.stats.calibratedEstimate.toFixed(3)} published (ring [${ring.values.map((v) => v.toFixed(3)).join(", ")}], n=${ring.values.length}) route=${settleOrigin}${ring.model ? ` model=${ring.model}` : ""} (#1933)`);
}
} else {
const hadPublished = session.stats.calibratedEstimate !== undefined;
delete session.stats.calibratedEstimate;
delete session.stats.calibratedEstimateOrigin;
delete session.stats.calibratedEstimateModel;
if (hadPublished) loggerLog("info", `[${session.id}] calibration k̂ cleared (n=${ring.values.length}, spread ${spread.toFixed(2)} > ${CALIBRATION_CONSISTENCY_RATIO}) — falling back to raw estimate until evidence agrees (#1933)`);
}
} else {
loggerLog("debug", `[${session.id}] calibration sample ${rawSample.toFixed(3)} rejected — outside plausibility band [${CALIBRATION_SAMPLE_MIN}, ${CALIBRATION_SAMPLE_MAX}] (billed ${billed} / local est ${Math.round(pendingEst)}) (#1933)`);
}
session.stats.calibrationRing = ring;
}
Expand All @@ -1824,7 +1841,9 @@ export function settleOverflowCalibration(
upstream?: string,
): void {
if (!(measured > 0)) return;
const pendingBefore = session.stats.lastLocalTextEstimate;
ingestCalibrationPair(session, measured, upstream);
loggerLog("info", `[${session.id}] overflow-stated measurement ingested: upstream stated ${measured} tokens for the rejected payload, local est ${pendingBefore !== undefined ? Math.round(pendingBefore) : "none"} (route=${normalizeUpstreamOrigin(upstream) ?? "?"}) — ring discipline applies (#3900/#5373)`);
// The rejected request will never produce a usage report — retire its
// pending pair NOW so no later report can mispair against a consumed
// estimate (#1933 F1). A same-request refold (#1195) re-runs prepare and
Expand Down
28 changes: 28 additions & 0 deletions src/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ interface PersistedSession {
calibratedEstimate?: number;
calibratedEstimateOrigin?: string;
calibrationRing?: { origin: string; values: number[] };
overflowBias?: { ratio: number; origin: string; model?: string; measured: number; estimate: number; mismatched?: boolean };
pendingFoldUsage?: boolean;
lastLocalTextEstimate?: number;
lastLocalTextEstimateOrigin?: string;
Expand Down Expand Up @@ -4050,6 +4051,32 @@ function restoreBlockContents(entries: Iterable<readonly [string, unknown]>): Ma
return blockContents;
}

// #5373: same discipline for the provisional overflow-stated bias — every
// field validated, ratio clamped into the published band, non-positive
// measurements dropped. A corrupt entry must degrade to absent (raw estimate),
// never steer a decision off-scale.
function restoreOverflowBias(v: unknown): { ratio: number; origin: string; model?: string; measured: number; estimate: number; mismatched?: boolean } | undefined {
if (!v || typeof v !== "object") return undefined;
const b = v as Record<string, unknown>;
if (typeof b.origin !== "string" || b.origin.length === 0) return undefined;
if (typeof b.ratio !== "number" || !Number.isFinite(b.ratio) || b.ratio <= 0) return undefined;
if (typeof b.measured !== "number" || !Number.isFinite(b.measured) || b.measured <= 0) return undefined;
if (typeof b.estimate !== "number" || !Number.isFinite(b.estimate) || b.estimate <= 0) return undefined;
// A stored ratio outside the band can only have come from an old writer;
// re-derive the distrust flag from the clamped value's neighbors so a
// corrupt file never steers decisions off-scale.
const clamped = Math.min(CALIBRATION_CLAMP_MAX, Math.max(CALIBRATION_CLAMP_MIN, b.ratio));
const mismatched = typeof b.mismatched === "boolean" ? b.mismatched : b.ratio < CALIBRATION_CLAMP_MIN || b.ratio > CALIBRATION_CLAMP_MAX;
return {
ratio: clamped,
origin: b.origin,
...(typeof b.model === "string" && b.model.length > 0 ? { model: b.model } : {}),
measured: b.measured,
estimate: b.estimate,
...(mismatched ? { mismatched: true } : {}),
};
}

/** #4942: the record→legacy-view adapter the sidecar ingest path runs on
* every committed record — exported so the hermetic seam suite can build a
* sidecar against a stub backend with the SAME adapter the store uses. */
Expand Down Expand Up @@ -4116,6 +4143,7 @@ function buildSession(parsed: PersistedSession): Session {
calibratedEstimate: restoreCalibratedEstimate(stats.calibratedEstimate),
calibratedEstimateOrigin: typeof stats.calibratedEstimateOrigin === "string" && stats.calibratedEstimateOrigin.trim().length > 0 ? stats.calibratedEstimateOrigin : undefined,
calibrationRing: restoreCalibrationRing(stats.calibrationRing),
overflowBias: restoreOverflowBias(stats.overflowBias),
// Intentionally NOT restored (#2129 triage): pendingFoldUsage is set at
// compress execution and consumed only as a log-suffix attribution by the
// NEXT usage report (#695) — restoring a stale true would mislabel an
Expand Down
64 changes: 57 additions & 7 deletions src/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { epochConfirmPending } from "./fold-reconcile.js";
import { deadSkipKeys, filterDeadRanges, parseStructuralEmptyVerdict, recordDeadRange } from "./dead-ranges.js";
import { peekRegistryOutputLimit } from "./registry.js";
import { safePrefix } from "./text-safe.js";
import { applyEstimateCalibration, currentCalibrationFactor } from "./util.js";
import { applyEstimateCalibration, calibrationOriginApplies, currentCalibrationFactor, effectiveCalibration } from "./util.js";
import { configuredSummaryPlan, type ConfiguredSummaryPlan } from "./external-summary-runtime.js";
import { messageTimestampSections } from "./external-summary-surface.js";
import { isCodexClient } from "./codex-compact.js";
Expand Down Expand Up @@ -1765,13 +1765,37 @@ export async function preflightCompress(deps: PreflightDeps, messages: CoreMessa
// kernel's own truncation/absorb behavior stays byte-identical.
const imageReserve = deps.imageReserve ?? 0;
const wireOverhead = deps.wireOverhead ?? 0;
// #1933 F1: capture k̂ once — it only mutates on usage settlement (outside
// this invocation), while every fit judgment in this loop must stay on
// one consistent scale with the gate that started it.
// #1933 F1: capture k̂ once — it only mutates on usage/overflow settlement
// (outside this invocation), while every fit judgment in this loop must
// stay on one consistent scale with the gate that started it.
// #2117 B: model-provenance gate — a factor learned on another model acts
// as absent (raw estimate) rather than deciding with a cross-model scale.
const kFactor = currentCalibrationFactor(deps.session.stats, deps.session.metadata?.lastModel);
const kOrigin = deps.session.stats.calibratedEstimateOrigin;
// #5373: effectiveCalibration falls back to the provisional overflow-stated
// bias when the ring has nothing applicable to this route, so an
// overflow-only session folds on the upstream-stated scale from the first
// rejection onward.
let calibration = effectiveCalibration(deps.session.stats, deps.session.metadata?.lastModel, deps.upstreamOrigin);
// #1195 B: an UNCLAMPED overflow arm — the measured count already exceeds
// the stated ceiling (declared window wider than the provider's real one) —
// keeps master's raw-scale fit judgments. The provisional bias there declares
// an unfoldable payload over-limit, the walk books a non-materializable
// emergency-truncate credit, and the #1987 anchor then clobbers the
// upstream-measured arm baseline (source flips to "estimate"). Only the
// ring-published factor (master's established behavior) may steer such a
// session; clamped arms and post-retire sessions steer at stated calibre.
{
const s = deps.session.stats;
const unclampedArm = s.lastInputTokensSource === "overflow-arm"
&& !(s.overflowArmTokens !== undefined && s.lastInputTokens < s.overflowArmTokens);
if (unclampedArm) {
const ringK = currentCalibrationFactor(s, deps.session.metadata?.lastModel);
calibration = ringK !== undefined && calibrationOriginApplies(s.calibratedEstimateOrigin, deps.upstreamOrigin)
? { k: ringK, origin: s.calibratedEstimateOrigin }
: { k: undefined, origin: undefined };
}
}
const kFactor = calibration.k;
const kOrigin = calibration.origin;
// #3645: kernel range.tokens are the UNCALIBRATED count, but
// result.payloadEstimate (hence every deficit derived from it) sits on the
// CALIBRATED scale once a matching-origin k̂ is set (#1933 F1). Folding a
Expand Down Expand Up @@ -2269,8 +2293,34 @@ export async function preflightCompress(deps: PreflightDeps, messages: CoreMessa
// marathon — that burn was driven by a stale USAGE-sourced baseline
// measured under a prior wider-window model, which keeps the floorless
// roundText path.
// #5373 carve-out of the exception: when a route-calibration factor applies
// to this origin (ring-published k̂ or the provisional overflow-stated bias),
// roundText is ALREADY projected onto the upstream-stated scale above — the
// distrust premise ("the local estimate may not outrank the rejection") is
// exactly the gap the factor closes. Without it, a clamped arm makes the
// floored comparator unsatisfiable (floor == compressionTarget == stated
// budget ⇒ floor−img never < target−img) and the rescue burns the whole
// ladder, forwarding whatever remains over the window again. With the
// factor the calibrated roundText exits early at raw < target/k̂ — the
// stated budget expressed in local units. k̂>1 keeps the raw floor implied
// (calRound<target ⟹ rawRound<target/k̂≤target≤floor); k̂≤1 makes the
// calibrated clause strictly weaker than the raw one, so behavior only
// relaxes in the direction the measurement says is safe. No factor → the
// legacy floored path stays byte-identical.
// Clamped-arm restriction (#1195 B regression): the carve-out exists for
// CLAMPED arms — the baseline capped by the judging limit below the stated
// ceiling (overflowArmTokens) — where the floored comparator is provably
// unsatisfiable. An unclamped arm whose measured count already exceeds the
// stated ceiling (declared window wider than the provider's real one) has
// no foldable range when the payload is unfoldable; the floorless path
// there books a kernel emergency-truncate credit the rebuild cannot
// materialize, and the #1987 anchor then clobbers the upstream-measured
// arm baseline. Unclamped arms keep the legacy floored passthrough.
const armClamped = deps.session.stats.overflowArmTokens !== undefined
&& deps.session.stats.lastInputTokens < deps.session.stats.overflowArmTokens;
const armedFactorApplies = armClamped && kFactor !== undefined && calibrationOriginApplies(kOrigin, deps.upstreamOrigin);
const armedBaseline = deps.session.stats.lastInputTokensSource === "overflow-arm";
if (armedBaseline ? decisionTokens < textTarget : roundText < textTarget) {
if (armedBaseline && !armedFactorApplies ? decisionTokens < textTarget : roundText < textTarget) {
// #4288: the walk ends in a fit-forward — the caller's gate judges
// on the same floorless payloadEstimate and forwards. The final
// verdict below must not stamp an exhausted/failure verdict over
Expand Down
Loading
Loading