@@ -76,6 +76,13 @@ describe("runCliTurnCompactionLifecycle", () => {
7676
7777 beforeEach ( async ( ) => {
7878 tmpDir = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , "openclaw-cli-compaction-" ) ) ;
79+ // Default backends to non-owning so the context-engine compaction-path tests
80+ // exercise that path. On current main resolveCliBackendConfig("claude-cli")
81+ // resolves the (now ownsNativeCompaction) backend even in unit tests, which
82+ // would otherwise route every claude-cli compaction test through the #88315
83+ // defer no-op. The ownsNativeCompaction-specific tests override this with an
84+ // owning backend to exercise the defer.
85+ setCliCompactionTestDeps ( { resolveCliBackendConfig : ( ) => null } ) ;
7986 } ) ;
8087
8188 afterEach ( async ( ) => {
@@ -1406,4 +1413,212 @@ describe("runCliTurnCompactionLifecycle", () => {
14061413 "claude-session" ,
14071414 ) ;
14081415 } ) ;
1416+
1417+ it ( "skips compaction when backend declares ownsNativeCompaction and has no harness endpoint" , async ( ) => {
1418+ const sessionKey = "agent:main:claude-owns-compaction" ;
1419+ const sessionId = "session-claude-owns" ;
1420+ const sessionFile = path . join ( tmpDir , "session-claude-owns.jsonl" ) ;
1421+ const storePath = path . join ( tmpDir , "sessions-claude-owns.json" ) ;
1422+ await writeSessionFile ( { sessionFile, sessionId } ) ;
1423+
1424+ const sessionEntry : SessionEntry = {
1425+ sessionId,
1426+ updatedAt : Date . now ( ) ,
1427+ sessionFile,
1428+ contextTokens : 1_000 ,
1429+ totalTokens : 950 ,
1430+ totalTokensFresh : true ,
1431+ } ;
1432+ const sessionStore : Record < string , SessionEntry > = { [ sessionKey ] : sessionEntry } ;
1433+ await fs . writeFile ( storePath , JSON . stringify ( sessionStore , null , 2 ) , "utf-8" ) ;
1434+
1435+ const compactCalls : Array < Parameters < ContextEngine [ "compact" ] > [ 0 ] > = [ ] ;
1436+ const compactAgentHarnessSession = vi . fn ( ) ;
1437+ const recordCliCompactionInStore = vi . fn ( ) ;
1438+ setCliCompactionTestDeps ( {
1439+ resolveContextEngine : async ( ) => buildContextEngine ( { compactCalls } ) ,
1440+ maybeCompactAgentHarnessSession : compactAgentHarnessSession as never ,
1441+ resolveCliBackendConfig : ( ) => ( {
1442+ id : "claude-cli" ,
1443+ config : { command : "claude" } ,
1444+ bundleMcp : true ,
1445+ ownsNativeCompaction : true ,
1446+ } ) ,
1447+ createPreparedEmbeddedAgentSettingsManager : async ( ) => ( {
1448+ getCompactionReserveTokens : ( ) => 200 ,
1449+ getCompactionKeepRecentTokens : ( ) => 0 ,
1450+ applyOverrides : ( ) => { } ,
1451+ } ) ,
1452+ shouldPreemptivelyCompactBeforePrompt : ( ) => ( {
1453+ route : "fits" ,
1454+ shouldCompact : false ,
1455+ estimatedPromptTokens : 600 ,
1456+ promptBudgetBeforeReserve : 800 ,
1457+ overflowTokens : 0 ,
1458+ toolResultReducibleChars : 0 ,
1459+ effectiveReserveTokens : 200 ,
1460+ } ) ,
1461+ resolveLiveToolResultMaxChars : ( ) => 20_000 ,
1462+ recordCliCompactionInStore,
1463+ } ) ;
1464+
1465+ const updatedEntry = await runCliTurnCompactionLifecycle ( {
1466+ cfg : { } as OpenClawConfig ,
1467+ sessionId,
1468+ sessionKey,
1469+ sessionEntry,
1470+ sessionStore,
1471+ storePath,
1472+ sessionAgentId : "main" ,
1473+ workspaceDir : tmpDir ,
1474+ agentDir : tmpDir ,
1475+ provider : "claude-cli" ,
1476+ model : "opus" ,
1477+ } ) ;
1478+
1479+ expect ( compactAgentHarnessSession ) . not . toHaveBeenCalled ( ) ;
1480+ expect ( compactCalls ) . toHaveLength ( 0 ) ;
1481+ expect ( recordCliCompactionInStore ) . not . toHaveBeenCalled ( ) ;
1482+ expect ( updatedEntry ) . toBe ( sessionEntry ) ;
1483+ } ) ;
1484+
1485+ it ( "does not skip compaction when backend does not declare ownsNativeCompaction" , async ( ) => {
1486+ const sessionKey = "agent:main:generic-no-ownership" ;
1487+ const sessionId = "session-generic" ;
1488+ const sessionFile = path . join ( tmpDir , "session-generic.jsonl" ) ;
1489+ const storePath = path . join ( tmpDir , "sessions-generic.json" ) ;
1490+ await writeSessionFile ( { sessionFile, sessionId } ) ;
1491+
1492+ const sessionEntry : SessionEntry = {
1493+ sessionId,
1494+ updatedAt : Date . now ( ) ,
1495+ sessionFile,
1496+ contextTokens : 1_000 ,
1497+ totalTokens : 950 ,
1498+ totalTokensFresh : true ,
1499+ } ;
1500+ const sessionStore : Record < string , SessionEntry > = { [ sessionKey ] : sessionEntry } ;
1501+ await fs . writeFile ( storePath , JSON . stringify ( sessionStore , null , 2 ) , "utf-8" ) ;
1502+
1503+ const compactCalls : Array < Parameters < ContextEngine [ "compact" ] > [ 0 ] > = [ ] ;
1504+ const maintenance = vi . fn ( async ( ) => ( { changed : false , bytesFreed : 0 , rewrittenEntries : 0 } ) ) ;
1505+ setCliCompactionTestDeps ( {
1506+ resolveContextEngine : async ( ) => buildContextEngine ( { compactCalls } ) ,
1507+ resolveCliBackendConfig : ( ) => ( {
1508+ id : "generic-backend" ,
1509+ config : { command : "generic" } ,
1510+ bundleMcp : false ,
1511+ } ) ,
1512+ createPreparedEmbeddedAgentSettingsManager : async ( ) => ( {
1513+ getCompactionReserveTokens : ( ) => 200 ,
1514+ getCompactionKeepRecentTokens : ( ) => 0 ,
1515+ applyOverrides : ( ) => { } ,
1516+ } ) ,
1517+ shouldPreemptivelyCompactBeforePrompt : ( ) => ( {
1518+ route : "fits" ,
1519+ shouldCompact : false ,
1520+ estimatedPromptTokens : 600 ,
1521+ promptBudgetBeforeReserve : 800 ,
1522+ overflowTokens : 0 ,
1523+ toolResultReducibleChars : 0 ,
1524+ effectiveReserveTokens : 200 ,
1525+ } ) ,
1526+ resolveLiveToolResultMaxChars : ( ) => 20_000 ,
1527+ runContextEngineMaintenance : maintenance ,
1528+ } ) ;
1529+
1530+ await runCliTurnCompactionLifecycle ( {
1531+ cfg : { } as OpenClawConfig ,
1532+ sessionId,
1533+ sessionKey,
1534+ sessionEntry,
1535+ sessionStore,
1536+ storePath,
1537+ sessionAgentId : "main" ,
1538+ workspaceDir : tmpDir ,
1539+ agentDir : tmpDir ,
1540+ provider : "generic-backend" ,
1541+ model : "model" ,
1542+ } ) ;
1543+
1544+ expect ( compactCalls ) . toHaveLength ( 1 ) ;
1545+ } ) ;
1546+
1547+ it ( "still uses native harness path when backend declares ownsNativeCompaction and has agentHarnessId" , async ( ) => {
1548+ const sessionKey = "agent:main:codex-with-ownership" ;
1549+ const sessionId = "session-codex-ownership" ;
1550+ const sessionFile = path . join ( tmpDir , "session-codex-ownership.jsonl" ) ;
1551+ const storePath = path . join ( tmpDir , "sessions-codex-ownership.json" ) ;
1552+ await writeSessionFile ( { sessionFile, sessionId } ) ;
1553+
1554+ const sessionEntry : SessionEntry = {
1555+ sessionId,
1556+ updatedAt : Date . now ( ) ,
1557+ sessionFile,
1558+ contextTokens : 1_000 ,
1559+ totalTokens : 950 ,
1560+ totalTokensFresh : true ,
1561+ agentHarnessId : "codex" ,
1562+ } ;
1563+ const sessionStore : Record < string , SessionEntry > = { [ sessionKey ] : sessionEntry } ;
1564+ await fs . writeFile ( storePath , JSON . stringify ( sessionStore , null , 2 ) , "utf-8" ) ;
1565+
1566+ const compactCalls : Array < Parameters < ContextEngine [ "compact" ] > [ 0 ] > = [ ] ;
1567+ const contextEngine = buildContextEngine ( { compactCalls } ) ;
1568+ const compactAgentHarnessSession = vi . fn ( async ( ) => ( {
1569+ ok : true ,
1570+ compacted : true ,
1571+ result : { tokensBefore : 950 , tokensAfter : 100 } ,
1572+ } ) ) ;
1573+ const recordCliCompactionInStore = vi . fn ( async ( ) => ( {
1574+ ...sessionEntry ,
1575+ compactionCount : 1 ,
1576+ } ) ) ;
1577+ setCliCompactionTestDeps ( {
1578+ resolveContextEngine : async ( ) => contextEngine ,
1579+ ensureSelectedAgentHarnessPlugin : vi . fn ( async ( ) => undefined ) ,
1580+ maybeCompactAgentHarnessSession : compactAgentHarnessSession as never ,
1581+ resolveCliBackendConfig : ( ) => ( {
1582+ id : "codex" ,
1583+ config : { command : "codex" } ,
1584+ bundleMcp : false ,
1585+ ownsNativeCompaction : true ,
1586+ } ) ,
1587+ createPreparedEmbeddedAgentSettingsManager : async ( ) => ( {
1588+ getCompactionReserveTokens : ( ) => 200 ,
1589+ getCompactionKeepRecentTokens : ( ) => 0 ,
1590+ applyOverrides : ( ) => { } ,
1591+ } ) ,
1592+ shouldPreemptivelyCompactBeforePrompt : ( ) => ( {
1593+ route : "fits" ,
1594+ shouldCompact : false ,
1595+ estimatedPromptTokens : 600 ,
1596+ promptBudgetBeforeReserve : 800 ,
1597+ overflowTokens : 0 ,
1598+ toolResultReducibleChars : 0 ,
1599+ effectiveReserveTokens : 200 ,
1600+ } ) ,
1601+ resolveLiveToolResultMaxChars : ( ) => 20_000 ,
1602+ applyAgentAutoCompactionGuard : vi . fn ( async ( ) => ( { supported : true , disabled : false } ) ) ,
1603+ recordCliCompactionInStore,
1604+ } ) ;
1605+
1606+ await runCliTurnCompactionLifecycle ( {
1607+ cfg : { } as OpenClawConfig ,
1608+ sessionId,
1609+ sessionKey,
1610+ sessionEntry,
1611+ sessionStore,
1612+ storePath,
1613+ sessionAgentId : "main" ,
1614+ workspaceDir : tmpDir ,
1615+ agentDir : tmpDir ,
1616+ provider : "openai" ,
1617+ model : "gpt-5.5" ,
1618+ } ) ;
1619+
1620+ expect ( compactAgentHarnessSession ) . toHaveBeenCalledTimes ( 1 ) ;
1621+ expect ( compactCalls ) . toHaveLength ( 0 ) ;
1622+ expect ( recordCliCompactionInStore ) . toHaveBeenCalledTimes ( 1 ) ;
1623+ } ) ;
14091624} ) ;
0 commit comments