Skip to content

Commit c149fe4

Browse files
kewang-pikaclaude
andcommitted
fix: gate preflight compaction on totalTokensFresh === true
The fresh-token branch in runPreflightCompactionIfNeeded ran when shouldUseTranscriptFallback was false, but that predicate treated totalTokensFresh: undefined as NOT needing fallback (undefined === false evaluates to false). For legacy sessions where totalTokensFresh was never set, this caused the fresh-token path to use potentially stale persisted totals, triggering unnecessary compaction. Three changes: 1. Replace shouldUseTranscriptFallback with hasFreshPersistedTokens that requires totalTokensFresh === true explicitly. When undefined (legacy) or false (known-stale), falls through to transcript estimation. 2. In the transcript fallback branch, always run transcript estimation instead of skipping when resolveFreshSessionTotalTokens returns a value (that function also treats undefined freshness as fresh). 3. Add bail-out before shouldRunPreflightCompaction when transcript estimation returns no count and freshness is unconfirmed, preventing the gate function from falling back to stale totals via resolveFreshSessionTotalTokens. Resolves Codex P2 review on PR #66716. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 86352d7 commit c149fe4

2 files changed

Lines changed: 70 additions & 13 deletions

File tree

src/auto-reply/reply/agent-runner-memory.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,48 @@ describe("runPreflightCompactionIfNeeded", () => {
518518
expect(result).toBe(sessionEntry);
519519
});
520520

521+
it("skips compaction when totalTokensFresh is undefined (legacy sessions)", async () => {
522+
// Legacy session: totalTokensFresh was never set (undefined), but totalTokens
523+
// has a stale persisted value above the threshold. Previously this was treated
524+
// as fresh (undefined !== false → !shouldUseTranscriptFallback was true),
525+
// causing the fresh-token branch to use stale persisted totals and trigger
526+
// unnecessary compaction.
527+
const sessionEntry: SessionEntry = {
528+
sessionId: "session",
529+
updatedAt: Date.now(),
530+
totalTokens: 305_000,
531+
// totalTokensFresh is intentionally omitted (undefined)
532+
};
533+
const sessionStore = { main: sessionEntry };
534+
535+
const result = await runPreflightCompactionIfNeeded({
536+
cfg: {
537+
agents: {
538+
defaults: {
539+
compaction: {
540+
reserveTokensFloor: 30_000,
541+
},
542+
},
543+
},
544+
},
545+
followupRun: createFollowupRun(),
546+
defaultModel: "claude-sonnet-4-6",
547+
agentCfgContextTokens: 200_000,
548+
sessionEntry,
549+
sessionStore,
550+
sessionKey: "main",
551+
isHeartbeat: false,
552+
replyOperation: createReplyOperation(),
553+
});
554+
555+
// With undefined freshness and no transcript file to read, transcript
556+
// estimation returns undefined. The function bails out because there is
557+
// no reliable token count and freshness is not confirmed, rather than
558+
// falling back to stale persisted totals.
559+
expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled();
560+
expect(result).toBe(sessionEntry);
561+
});
562+
521563
it("skips compaction on heartbeat even with fresh tokens above threshold", async () => {
522564
const sessionEntry: SessionEntry = {
523565
sessionId: "session",

src/auto-reply/reply/agent-runner-memory.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,11 @@ export async function runPreflightCompactionIfNeeded(params: {
390390
typeof persistedTotalTokens === "number" &&
391391
Number.isFinite(persistedTotalTokens) &&
392392
persistedTotalTokens > 0;
393-
const shouldUseTranscriptFallback = entry.totalTokensFresh === false || !hasPersistedTotalTokens;
393+
// Only use persisted totals when totalTokensFresh is explicitly true.
394+
// When totalTokensFresh is undefined (legacy sessions where freshness was
395+
// never tracked), fall through to transcript estimation so we don't gate
396+
// compaction on potentially stale persisted values.
397+
const hasFreshPersistedTokens = hasPersistedTotalTokens && entry.totalTokensFresh === true;
394398

395399
// Resolve the token count for the compaction threshold check.
396400
//
@@ -404,14 +408,15 @@ export async function runPreflightCompactionIfNeeded(params: {
404408
// threshold, so compaction never triggered — even at 153% of the context
405409
// window — when the prompt cache absorbed all tokens. (#66520)
406410
//
407-
// When totalTokens is stale/unknown, fall back to reading the session
408-
// transcript and estimating token counts from the message content.
411+
// When totalTokens is stale/unknown (totalTokensFresh is false OR undefined),
412+
// fall back to reading the session transcript and estimating token counts
413+
// from the message content.
409414
const promptTokenEstimate = estimatePromptTokensForMemoryFlush(
410415
params.promptForEstimate ?? params.followupRun.prompt,
411416
);
412417
let tokenCountForCompaction: number | undefined;
413418
let transcriptPromptTokens: number | undefined;
414-
if (!shouldUseTranscriptFallback) {
419+
if (hasFreshPersistedTokens) {
415420
// Fresh persisted tokens available — project forward with prompt estimate
416421
const projectedFreshTokens = resolveEffectivePromptTokens(
417422
freshPersistedTokens,
@@ -425,15 +430,16 @@ export async function runPreflightCompactionIfNeeded(params: {
425430
? projectedFreshTokens
426431
: freshPersistedTokens;
427432
} else {
428-
// Stale/unknown — fall back to transcript estimation
429-
transcriptPromptTokens =
430-
typeof freshPersistedTokens === "number"
431-
? undefined
432-
: estimatePromptTokensFromSessionTranscript({
433-
sessionId: entry.sessionId,
434-
storePath: params.storePath,
435-
sessionFile: entry.sessionFile ?? params.followupRun.run.sessionFile,
436-
});
433+
// Stale/unknown — fall back to transcript estimation.
434+
// Always estimate from transcript when freshness is not confirmed,
435+
// even if resolveFreshSessionTotalTokens would return a value
436+
// (it treats totalTokensFresh: undefined as fresh, which is the
437+
// exact bug we're guarding against here).
438+
transcriptPromptTokens = estimatePromptTokensFromSessionTranscript({
439+
sessionId: entry.sessionId,
440+
storePath: params.storePath,
441+
sessionFile: entry.sessionFile ?? params.followupRun.run.sessionFile,
442+
});
437443
const projectedTokenCount =
438444
typeof transcriptPromptTokens === "number"
439445
? resolveEffectivePromptTokens(transcriptPromptTokens, undefined, promptTokenEstimate)
@@ -457,6 +463,15 @@ export async function runPreflightCompactionIfNeeded(params: {
457463
`promptTokensEst=${promptTokenEstimate ?? "undefined"}`,
458464
);
459465

466+
// If transcript estimation didn't produce a token count and persisted totals
467+
// are not confirmed fresh, we have no reliable basis for compaction gating.
468+
// Bail out rather than letting shouldRunPreflightCompaction fall back to
469+
// resolveFreshSessionTotalTokens (which treats totalTokensFresh: undefined
470+
// as fresh).
471+
if (typeof tokenCountForCompaction !== "number" && entry.totalTokensFresh !== true) {
472+
return entry ?? params.sessionEntry;
473+
}
474+
460475
const shouldCompact = shouldRunPreflightCompaction({
461476
entry,
462477
tokenCount: tokenCountForCompaction,

0 commit comments

Comments
 (0)