@@ -8,13 +8,18 @@ import {
88 registerMemoryFlushPlanResolver ,
99} from "../../plugins/memory-state.js" ;
1010import type { TemplateContext } from "../templating.js" ;
11- import { runMemoryFlushIfNeeded , setAgentRunnerMemoryTestDeps } from "./agent-runner-memory.js" ;
11+ import {
12+ runMemoryFlushIfNeeded ,
13+ runPreflightCompactionIfNeeded ,
14+ setAgentRunnerMemoryTestDeps ,
15+ } from "./agent-runner-memory.js" ;
1216import type { FollowupRun } from "./queue.js" ;
1317
1418const runWithModelFallbackMock = vi . fn ( ) ;
1519const runEmbeddedPiAgentMock = vi . fn ( ) ;
1620const refreshQueuedFollowupSessionMock = vi . fn ( ) ;
1721const incrementCompactionCountMock = vi . fn ( ) ;
22+ const compactEmbeddedPiSessionMock = vi . fn ( ) ;
1823
1924function createReplyOperation ( ) {
2025 return {
@@ -104,11 +109,13 @@ describe("runMemoryFlushIfNeeded", () => {
104109 }
105110 return nextEntry . compactionCount ;
106111 } ) ;
112+ compactEmbeddedPiSessionMock . mockReset ( ) . mockResolvedValue ( { ok : false , reason : "test_skip" } ) ;
107113 setAgentRunnerMemoryTestDeps ( {
108114 runWithModelFallback : runWithModelFallbackMock as never ,
109115 runEmbeddedPiAgent : runEmbeddedPiAgentMock as never ,
110116 refreshQueuedFollowupSession : refreshQueuedFollowupSessionMock as never ,
111117 incrementCompactionCount : incrementCompactionCountMock as never ,
118+ compactEmbeddedPiSession : compactEmbeddedPiSessionMock as never ,
112119 registerAgentRunContext : vi . fn ( ) as never ,
113120 randomUUID : ( ) => "00000000-0000-0000-0000-000000000001" ,
114121 now : ( ) => 1_700_000_000_000 ,
@@ -288,3 +295,85 @@ describe("runMemoryFlushIfNeeded", () => {
288295 expect ( flushCall . bootstrapPromptWarningSignature ) . toBe ( "sig-b" ) ;
289296 } ) ;
290297} ) ;
298+
299+ describe ( "runPreflightCompactionIfNeeded" , ( ) => {
300+ beforeEach ( ( ) => {
301+ registerMemoryFlushPlanResolver ( ( ) => ( {
302+ softThresholdTokens : 4_000 ,
303+ forceFlushTranscriptBytes : 1_000_000_000 ,
304+ reserveTokensFloor : 20_000 ,
305+ prompt : "flush" ,
306+ systemPrompt : "flush" ,
307+ relativePath : "memory/2023-11-14.md" ,
308+ } ) ) ;
309+ compactEmbeddedPiSessionMock . mockReset ( ) . mockResolvedValue ( { ok : false , reason : "test_skip" } ) ;
310+ setAgentRunnerMemoryTestDeps ( {
311+ compactEmbeddedPiSession : compactEmbeddedPiSessionMock as never ,
312+ registerAgentRunContext : vi . fn ( ) as never ,
313+ } ) ;
314+ } ) ;
315+
316+ afterEach ( ( ) => {
317+ setAgentRunnerMemoryTestDeps ( ) ;
318+ clearMemoryPluginState ( ) ;
319+ } ) ;
320+
321+ // Regression for #63892: after the first compaction, `totalTokensFresh` is
322+ // set to `true`. The early-return gate used to short-circuit unconditionally
323+ // in that state, so proactive compaction never re-fired once fresh tokens
324+ // grew back past the threshold. The gate must now also check that fresh
325+ // persisted tokens are still below the compaction threshold before skipping.
326+ it ( "re-runs preflight when totalTokensFresh is true but tokens exceed threshold (#63892)" , async ( ) => {
327+ const sessionKey = "main" ;
328+ const sessionEntry : SessionEntry = {
329+ sessionId : "session" ,
330+ updatedAt : Date . now ( ) ,
331+ totalTokens : 90_000 ,
332+ totalTokensFresh : true ,
333+ compactionCount : 1 ,
334+ } ;
335+
336+ await runPreflightCompactionIfNeeded ( {
337+ cfg : { agents : { defaults : { compaction : { } } } } ,
338+ followupRun : createFollowupRun ( ) ,
339+ defaultModel : "anthropic/claude-opus-4-6" ,
340+ agentCfgContextTokens : 100_000 ,
341+ sessionEntry,
342+ sessionStore : { [ sessionKey ] : sessionEntry } ,
343+ sessionKey,
344+ isHeartbeat : false ,
345+ replyOperation : createReplyOperation ( ) ,
346+ } ) ;
347+
348+ // threshold = 100_000 - 20_000 - 4_000 = 76_000; 90_000 > 76_000, so the
349+ // early-return must fall through and compaction must be attempted.
350+ expect ( compactEmbeddedPiSessionMock ) . toHaveBeenCalledTimes ( 1 ) ;
351+ } ) ;
352+
353+ it ( "skips preflight when totalTokensFresh is true and tokens are below threshold" , async ( ) => {
354+ const sessionKey = "main" ;
355+ const sessionEntry : SessionEntry = {
356+ sessionId : "session" ,
357+ updatedAt : Date . now ( ) ,
358+ totalTokens : 10_000 ,
359+ totalTokensFresh : true ,
360+ compactionCount : 1 ,
361+ } ;
362+
363+ const result = await runPreflightCompactionIfNeeded ( {
364+ cfg : { agents : { defaults : { compaction : { } } } } ,
365+ followupRun : createFollowupRun ( ) ,
366+ defaultModel : "anthropic/claude-opus-4-6" ,
367+ agentCfgContextTokens : 100_000 ,
368+ sessionEntry,
369+ sessionStore : { [ sessionKey ] : sessionEntry } ,
370+ sessionKey,
371+ isHeartbeat : false ,
372+ replyOperation : createReplyOperation ( ) ,
373+ } ) ;
374+
375+ // Fresh tokens are below threshold — the fast-path early return must fire.
376+ expect ( compactEmbeddedPiSessionMock ) . not . toHaveBeenCalled ( ) ;
377+ expect ( result ) . toBe ( sessionEntry ) ;
378+ } ) ;
379+ } ) ;
0 commit comments