@@ -15,6 +15,13 @@ import {
1515 ensureContextEnginesInitialized ,
1616 resolveContextEngine ,
1717} from "../../context-engine/index.js" ;
18+ import {
19+ captureCompactionCheckpointSnapshot ,
20+ cleanupCompactionCheckpointSnapshot ,
21+ persistSessionCompactionCheckpoint ,
22+ resolveSessionCompactionCheckpointReason ,
23+ type CapturedCompactionCheckpointSnapshot ,
24+ } from "../../gateway/session-compaction-checkpoints.js" ;
1825import { resolveHeartbeatSummaryForAgent } from "../../infra/heartbeat-summary.js" ;
1926import { getMachineDisplayName } from "../../infra/machine-name.js" ;
2027import { generateSecureToken } from "../../infra/secure-random.js" ;
@@ -108,6 +115,7 @@ import { applyExtraParamsToAgent } from "./extra-params.js";
108115import { getDmHistoryLimitFromSessionKey , limitHistoryTurns } from "./history.js" ;
109116import { resolveGlobalLane , resolveSessionLane } from "./lanes.js" ;
110117import { log } from "./logger.js" ;
118+ import { hardenManualCompactionBoundary } from "./manual-compaction-boundary.js" ;
111119import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-discovery-input.js" ;
112120import { readPiModelContextTokens } from "./model-context-tokens.js" ;
113121import { buildModelAliasLines , resolveModelAsync } from "./model.js" ;
@@ -415,6 +423,8 @@ export async function compactEmbeddedPiSessionDirect(
415423
416424 let restoreSkillEnv : ( ( ) => void ) | undefined ;
417425 let compactionSessionManager : unknown = null ;
426+ let checkpointSnapshot : CapturedCompactionCheckpointSnapshot | null = null ;
427+ let checkpointSnapshotRetained = false ;
418428 try {
419429 const { shouldLoadSkillEntries, skillEntries } = resolveEmbeddedRunSkillEntries ( {
420430 workspaceDir : effectiveWorkspace ,
@@ -730,6 +740,10 @@ export async function compactEmbeddedPiSessionDirect(
730740 allowSyntheticToolResults : transcriptPolicy . allowSyntheticToolResults ,
731741 allowedToolNames,
732742 } ) ;
743+ checkpointSnapshot = captureCompactionCheckpointSnapshot ( {
744+ sessionManager,
745+ sessionFile : params . sessionFile ,
746+ } ) ;
733747 compactionSessionManager = sessionManager ;
734748 trackSessionManagerAccess ( params . sessionFile ) ;
735749 const settingsManager = createPreparedEmbeddedPiSettingsManager ( {
@@ -960,6 +974,28 @@ export async function compactEmbeddedPiSessionDirect(
960974 sessionKey : params . sessionKey ,
961975 sessionFile : params . sessionFile ,
962976 } ) ;
977+ let effectiveFirstKeptEntryId = result . firstKeptEntryId ;
978+ let postCompactionLeafId =
979+ typeof sessionManager . getLeafId === "function"
980+ ? ( sessionManager . getLeafId ( ) ?? undefined )
981+ : undefined ;
982+ if ( params . trigger === "manual" ) {
983+ try {
984+ const hardenedBoundary = await hardenManualCompactionBoundary ( {
985+ sessionFile : params . sessionFile ,
986+ } ) ;
987+ if ( hardenedBoundary . applied ) {
988+ effectiveFirstKeptEntryId =
989+ hardenedBoundary . firstKeptEntryId ?? effectiveFirstKeptEntryId ;
990+ postCompactionLeafId = hardenedBoundary . leafId ?? postCompactionLeafId ;
991+ session . agent . state . messages = hardenedBoundary . messages ;
992+ }
993+ } catch ( err ) {
994+ log . warn ( "[compaction] failed to harden manual compaction boundary" , {
995+ errorMessage : err instanceof Error ? err . message : String ( err ) ,
996+ } ) ;
997+ }
998+ }
963999 // Estimate tokens after compaction by summing token estimates for remaining messages
9641000 const tokensAfter = estimateTokensAfterCompaction ( {
9651001 messagesAfter : session . messages ,
@@ -969,6 +1005,32 @@ export async function compactEmbeddedPiSessionDirect(
9691005 } ) ;
9701006 const messageCountAfter = session . messages . length ;
9711007 const compactedCount = Math . max ( 0 , messageCountCompactionInput - messageCountAfter ) ;
1008+ if ( params . config && params . sessionKey && checkpointSnapshot ) {
1009+ try {
1010+ const storedCheckpoint = await persistSessionCompactionCheckpoint ( {
1011+ cfg : params . config ,
1012+ sessionKey : params . sessionKey ,
1013+ sessionId : params . sessionId ,
1014+ reason : resolveSessionCompactionCheckpointReason ( {
1015+ trigger : params . trigger ,
1016+ } ) ,
1017+ snapshot : checkpointSnapshot ,
1018+ summary : result . summary ,
1019+ firstKeptEntryId : effectiveFirstKeptEntryId ,
1020+ tokensBefore : observedTokenCount ?? result . tokensBefore ,
1021+ tokensAfter,
1022+ postSessionFile : params . sessionFile ,
1023+ postLeafId : postCompactionLeafId ,
1024+ postEntryId : postCompactionLeafId ,
1025+ createdAt : compactStartedAt ,
1026+ } ) ;
1027+ checkpointSnapshotRetained = storedCheckpoint !== null ;
1028+ } catch ( err ) {
1029+ log . warn ( "failed to persist compaction checkpoint" , {
1030+ errorMessage : err instanceof Error ? err . message : String ( err ) ,
1031+ } ) ;
1032+ }
1033+ }
9721034 const postMetrics = diagEnabled
9731035 ? summarizeCompactionMessages ( session . messages )
9741036 : undefined ;
@@ -1000,7 +1062,7 @@ export async function compactEmbeddedPiSessionDirect(
10001062 sessionFile : params . sessionFile ,
10011063 summaryLength : typeof result . summary === "string" ? result . summary . length : undefined ,
10021064 tokensBefore : result . tokensBefore ,
1003- firstKeptEntryId : result . firstKeptEntryId ,
1065+ firstKeptEntryId : effectiveFirstKeptEntryId ,
10041066 } ) ;
10051067 // Truncate session file to remove compacted entries (#39953)
10061068 if ( params . config ?. agents ?. defaults ?. compaction ?. truncateAfterCompaction ) {
@@ -1032,7 +1094,7 @@ export async function compactEmbeddedPiSessionDirect(
10321094 compacted : true ,
10331095 result : {
10341096 summary : result . summary ,
1035- firstKeptEntryId : result . firstKeptEntryId ,
1097+ firstKeptEntryId : effectiveFirstKeptEntryId ,
10361098 tokensBefore : observedTokenCount ?? result . tokensBefore ,
10371099 tokensAfter,
10381100 details : result . details ,
@@ -1091,6 +1153,9 @@ export async function compactEmbeddedPiSessionDirect(
10911153 } ) ;
10921154 return fail ( reason ) ;
10931155 } finally {
1156+ if ( ! checkpointSnapshotRetained ) {
1157+ await cleanupCompactionCheckpointSnapshot ( checkpointSnapshot ) ;
1158+ }
10941159 restoreSkillEnv ?.( ) ;
10951160 }
10961161}
@@ -1116,6 +1181,8 @@ export async function compactEmbeddedPiSession(
11161181 } ) ;
11171182 ensureContextEnginesInitialized ( ) ;
11181183 const contextEngine = await resolveContextEngine ( params . config ) ;
1184+ let checkpointSnapshot : CapturedCompactionCheckpointSnapshot | null = null ;
1185+ let checkpointSnapshotRetained = false ;
11191186 try {
11201187 const agentDir = params . agentDir ?? resolveOpenClawAgentDir ( ) ;
11211188 const resolvedCompactionTarget = resolveEmbeddedCompactionTarget ( {
@@ -1150,6 +1217,12 @@ export async function compactEmbeddedPiSession(
11501217 // Fire before_compaction / after_compaction hooks here so plugin subscribers
11511218 // are notified regardless of which engine is active.
11521219 const engineOwnsCompaction = contextEngine . info . ownsCompaction === true ;
1220+ checkpointSnapshot = engineOwnsCompaction
1221+ ? captureCompactionCheckpointSnapshot ( {
1222+ sessionManager : SessionManager . open ( params . sessionFile ) ,
1223+ sessionFile : params . sessionFile ,
1224+ } )
1225+ : null ;
11531226 const hookRunner = engineOwnsCompaction
11541227 ? asCompactionHookRunner ( getGlobalHookRunner ( ) )
11551228 : null ;
@@ -1222,6 +1295,33 @@ export async function compactEmbeddedPiSession(
12221295 runtimeContext,
12231296 } ) ;
12241297 if ( result . ok && result . compacted ) {
1298+ if ( params . config && params . sessionKey && checkpointSnapshot ) {
1299+ try {
1300+ const postCompactionSession = SessionManager . open ( params . sessionFile ) ;
1301+ const postLeafId = postCompactionSession . getLeafId ( ) ?? undefined ;
1302+ const storedCheckpoint = await persistSessionCompactionCheckpoint ( {
1303+ cfg : params . config ,
1304+ sessionKey : params . sessionKey ,
1305+ sessionId : params . sessionId ,
1306+ reason : resolveSessionCompactionCheckpointReason ( {
1307+ trigger : params . trigger ,
1308+ } ) ,
1309+ snapshot : checkpointSnapshot ,
1310+ summary : result . result ?. summary ,
1311+ firstKeptEntryId : result . result ?. firstKeptEntryId ,
1312+ tokensBefore : result . result ?. tokensBefore ,
1313+ tokensAfter : result . result ?. tokensAfter ,
1314+ postSessionFile : params . sessionFile ,
1315+ postLeafId,
1316+ postEntryId : postLeafId ,
1317+ } ) ;
1318+ checkpointSnapshotRetained = storedCheckpoint !== null ;
1319+ } catch ( err ) {
1320+ log . warn ( "failed to persist compaction checkpoint" , {
1321+ errorMessage : err instanceof Error ? err . message : String ( err ) ,
1322+ } ) ;
1323+ }
1324+ }
12251325 await runContextEngineMaintenance ( {
12261326 contextEngine,
12271327 sessionId : params . sessionId ,
@@ -1275,6 +1375,9 @@ export async function compactEmbeddedPiSession(
12751375 : undefined ,
12761376 } ;
12771377 } finally {
1378+ if ( ! checkpointSnapshotRetained ) {
1379+ await cleanupCompactionCheckpointSnapshot ( checkpointSnapshot ) ;
1380+ }
12781381 await contextEngine . dispose ?.( ) ;
12791382 }
12801383 } ) ,
@@ -1287,6 +1390,7 @@ export const __testing = {
12871390 containsRealConversationMessages,
12881391 estimateTokensAfterCompaction,
12891392 buildBeforeCompactionHookMetrics,
1393+ hardenManualCompactionBoundary,
12901394 runBeforeCompactionHooks,
12911395 runAfterCompactionHooks,
12921396 runPostCompactionSideEffects,
0 commit comments