@@ -20,6 +20,7 @@ import {
2020 replaceSessionEntry ,
2121 resolveSessionTranscriptRuntimeReadTarget ,
2222 resolveSessionTranscriptRuntimeTarget ,
23+ trimSessionTranscriptForManualCompact ,
2324 updateSessionEntry ,
2425 upsertSessionEntry ,
2526} from "./session-accessor.js" ;
@@ -543,6 +544,102 @@ describe("session accessor file-backed seam", () => {
543544 expect ( listSessionEntries ( { storePath } ) ) . toEqual ( [ ] ) ;
544545 } ) ;
545546
547+ it ( "trims a manual compact transcript and clears stale token metadata" , async ( ) => {
548+ const sessionId = "11111111-1111-4111-8111-111111111111" ;
549+ const manualTranscriptPath = path . join ( tempDir , `${ sessionId } .jsonl` ) ;
550+ const scope = {
551+ agentId : "main" ,
552+ sessionId,
553+ sessionKey : "agent:main:main" ,
554+ storePath,
555+ } ;
556+ const contextBudgetStatus : NonNullable < SessionEntry [ "contextBudgetStatus" ] > = {
557+ schemaVersion : 1 ,
558+ source : "pre-prompt-estimate" ,
559+ updatedAt : 90 ,
560+ provider : "openai" ,
561+ model : "gpt-5.5" ,
562+ route : "fits" ,
563+ shouldCompact : false ,
564+ estimatedPromptTokens : 10 ,
565+ contextTokenBudget : 100 ,
566+ promptBudgetBeforeReserve : 80 ,
567+ reserveTokens : 20 ,
568+ effectiveReserveTokens : 20 ,
569+ remainingPromptBudgetTokens : 70 ,
570+ overflowTokens : 0 ,
571+ toolResultReducibleChars : 0 ,
572+ messageCount : 1 ,
573+ unwindowedMessageCount : 1 ,
574+ } ;
575+ await upsertSessionEntry ( scope , {
576+ contextBudgetStatus,
577+ inputTokens : 10 ,
578+ outputTokens : 20 ,
579+ sessionFile : manualTranscriptPath ,
580+ sessionId,
581+ totalTokens : 30 ,
582+ totalTokensFresh : true ,
583+ updatedAt : 100 ,
584+ } ) ;
585+ fs . writeFileSync ( manualTranscriptPath , "line 1\n\nline 2\nline 3\nline 4\n" , "utf-8" ) ;
586+ const updates : unknown [ ] = [ ] ;
587+ const unsubscribe = onSessionTranscriptUpdate ( ( update ) => updates . push ( update ) ) ;
588+
589+ const result = await trimSessionTranscriptForManualCompact ( scope , {
590+ maxLines : 2 ,
591+ nowMs : 500 ,
592+ } ) ;
593+
594+ unsubscribe ( ) ;
595+ expect ( result ) . toMatchObject ( { compacted : true , kept : 2 } ) ;
596+ const archived = result . compacted ? result . archived : "" ;
597+ expect ( path . basename ( archived ) ) . toMatch ( new RegExp ( `^${ sessionId } \\.jsonl\\.bak\\.` ) ) ;
598+ expect ( fs . readFileSync ( archived , "utf-8" ) ) . toBe ( "line 1\n\nline 2\nline 3\nline 4\n" ) ;
599+ expect ( fs . readFileSync ( manualTranscriptPath , "utf-8" ) ) . toBe ( "line 3\nline 4\n" ) ;
600+ const updatedEntry = loadSessionEntry ( scope ) ;
601+ expect ( updatedEntry ) . toMatchObject ( {
602+ sessionFile : manualTranscriptPath ,
603+ sessionId,
604+ updatedAt : 500 ,
605+ } ) ;
606+ expect ( updatedEntry ?. contextBudgetStatus ) . toBeUndefined ( ) ;
607+ expect ( updatedEntry ?. inputTokens ) . toBeUndefined ( ) ;
608+ expect ( updatedEntry ?. outputTokens ) . toBeUndefined ( ) ;
609+ expect ( updatedEntry ?. totalTokens ) . toBeUndefined ( ) ;
610+ expect ( updatedEntry ?. totalTokensFresh ) . toBeUndefined ( ) ;
611+ expect ( updates ) . toEqual ( [ { sessionFile : archived } ] ) ;
612+ } ) ;
613+
614+ it ( "prefers the current generated transcript over a stale generated sessionFile" , async ( ) => {
615+ const currentSessionId = "11111111-1111-4111-8111-111111111111" ;
616+ const staleSessionId = "22222222-2222-4222-8222-222222222222" ;
617+ const currentTranscriptPath = path . join ( tempDir , `${ currentSessionId } .jsonl` ) ;
618+ const staleTranscriptPath = path . join ( tempDir , `${ staleSessionId } .jsonl` ) ;
619+ const scope = {
620+ agentId : "main" ,
621+ sessionId : currentSessionId ,
622+ sessionKey : "agent:main:main" ,
623+ storePath,
624+ } ;
625+ await upsertSessionEntry ( scope , {
626+ sessionFile : staleTranscriptPath ,
627+ sessionId : currentSessionId ,
628+ updatedAt : 100 ,
629+ } ) ;
630+ fs . writeFileSync ( currentTranscriptPath , "current one\ncurrent two\n" , "utf-8" ) ;
631+ fs . writeFileSync ( staleTranscriptPath , "stale one\nstale two\n" , "utf-8" ) ;
632+
633+ const result = await trimSessionTranscriptForManualCompact ( scope , {
634+ maxLines : 1 ,
635+ sessionFile : staleTranscriptPath ,
636+ } ) ;
637+
638+ expect ( result ) . toMatchObject ( { compacted : true , kept : 1 } ) ;
639+ expect ( fs . readFileSync ( currentTranscriptPath , "utf-8" ) ) . toBe ( "current two\n" ) ;
640+ expect ( fs . readFileSync ( staleTranscriptPath , "utf-8" ) ) . toBe ( "stale one\nstale two\n" ) ;
641+ } ) ;
642+
546643 it ( "rejects transcript writes without a session key or explicit file" , async ( ) => {
547644 await expect (
548645 appendTranscriptEvent ( { sessionId : "session-1" , storePath } , { type : "metadata" } ) ,
0 commit comments