diff --git a/apps/desktop/src/main/__tests__/context-budget-policy.test.ts b/apps/desktop/src/main/__tests__/context-budget-policy.test.ts index ee1e069728..6a880f37d4 100644 --- a/apps/desktop/src/main/__tests__/context-budget-policy.test.ts +++ b/apps/desktop/src/main/__tests__/context-budget-policy.test.ts @@ -146,3 +146,84 @@ describe('desktop activeToolResultPrune policy', () => { assert.equal(policy?.maxHistoryEstimatedTokens, 128_000 - 4096); }); }); + +const STALE_PRUNE_ENV_KEYS = [ + 'MAKA_CONTEXT_BUDGET', + 'MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE', + 'MAKA_CONTEXT_STALE_TOOL_RESULT_MAX_TOKENS', + 'MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS', + 'MAKA_CONTEXT_MIN_RECENT_TURNS', +] as const; + +describe('desktop staleToolResultPrune policy', () => { + const savedStaleEnv: Record = {}; + + beforeEach(() => { + for (const key of STALE_PRUNE_ENV_KEYS) { + savedStaleEnv[key] = process.env[key]; + delete process.env[key]; + } + }); + + afterEach(() => { + for (const key of STALE_PRUNE_ENV_KEYS) { + if (savedStaleEnv[key] === undefined) delete process.env[key]; + else process.env[key] = savedStaleEnv[key]; + } + }); + + test('is enabled by default with the measured 2048-token threshold', () => { + const policy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(policy?.staleToolResultPrune?.enabled, true); + assert.equal(policy?.staleToolResultPrune?.maxResultEstimatedTokens, 2048); + assert.equal(policy?.staleToolResultPrune?.minRecentTurnsFull, 2); + }); + + test('can be disabled with explicit false', () => { + process.env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE = 'false'; + const policy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(policy?.staleToolResultPrune, undefined); + }); + + test('can be disabled with explicit off', () => { + process.env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE = 'off'; + const policy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(policy?.staleToolResultPrune, undefined); + }); + + test('accepts standard truthy values as enabled', () => { + process.env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE = 'true'; + const policy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(policy?.staleToolResultPrune?.enabled, true); + }); + + test('rejects malformed boolean values instead of silently disabling', () => { + process.env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE = 'maybe'; + assert.throws( + () => buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }), + /MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE must be a boolean/, + ); + }); + + test('respects max result estimated tokens env', () => { + process.env.MAKA_CONTEXT_STALE_TOOL_RESULT_MAX_TOKENS = '4096'; + const policy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(policy?.staleToolResultPrune?.maxResultEstimatedTokens, 4096); + }); + + test('respects min recent turns full env with MAKA_CONTEXT_MIN_RECENT_TURNS fallback', () => { + process.env.MAKA_CONTEXT_MIN_RECENT_TURNS = '3'; + const fallbackPolicy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(fallbackPolicy?.staleToolResultPrune?.minRecentTurnsFull, 3); + + process.env.MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS = '5'; + const explicitPolicy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(explicitPolicy?.staleToolResultPrune?.minRecentTurnsFull, 5); + }); + + test('MAKA_CONTEXT_BUDGET=off disables the whole policy including staleToolResultPrune', () => { + process.env.MAKA_CONTEXT_BUDGET = 'off'; + const policy = buildDefaultContextBudgetPolicy(openaiConnection(), { name: 'desktop-default-history-budget' }); + assert.equal(policy, undefined); + }); +}); diff --git a/packages/headless/src/__tests__/harbor-cell.test.ts b/packages/headless/src/__tests__/harbor-cell.test.ts index dd0976dc89..072ddc0b65 100644 --- a/packages/headless/src/__tests__/harbor-cell.test.ts +++ b/packages/headless/src/__tests__/harbor-cell.test.ts @@ -1430,7 +1430,7 @@ describe('runHarborCell', () => { }); test('Harbor context budget env treats explicit false-like booleans as disabled', () => { - // stale and archive default off; activeToolResultPrune defaults on, so disabling + // archive retrieval defaults off; stale and active prune default on, so disabling // stale/archive alone still leaves an enabled activeToolResultPrune policy. assert.deepEqual( buildHarborCellContextBudgetBackendOptions({ MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE: 'false' }).contextBudget?.activeToolResultPrune, @@ -1471,14 +1471,49 @@ describe('runHarborCell', () => { }); test('Harbor active tool result prune can be disabled with explicit off', () => { - assert.equal( - buildHarborCellContextBudgetBackendOptions({ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE: 'off' }).contextBudget, - undefined, - ); - assert.equal( - buildHarborCellContextBudgetPolicySnapshot({ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE: 'off' }), - undefined, - ); + const options = buildHarborCellContextBudgetBackendOptions({ MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE: 'off' }); + assert.equal(options.contextBudget?.activeToolResultPrune, undefined); + assert.deepEqual(options.contextBudget?.staleToolResultPrune, { enabled: true, minRecentTurnsFull: 2 }); + }); + + test('Harbor stale tool result prune is enabled by default without any env', () => { + const backend = buildHarborCellContextBudgetBackendOptions({}); + // minRecentTurnsFull is set explicitly so the runtime protection window + // matches the policy snapshot and desktop default instead of the runtime's + // internal ?? 1 fallback. + assert.deepEqual(backend.contextBudget?.staleToolResultPrune, { enabled: true, minRecentTurnsFull: 2 }); + + const snapshot = buildHarborCellContextBudgetPolicySnapshot({}); + assert.equal(snapshot?.enabled, true); + assert.equal(snapshot?.staleToolResultPrune?.enabled, true); + assert.equal(snapshot?.staleToolResultPrune?.maxResultEstimatedTokens, 2048); + assert.equal(snapshot?.staleToolResultPrune?.minRecentTurnsFull, 2); + }); + + test('Harbor stale tool result prune can be disabled with explicit off', () => { + const options = buildHarborCellContextBudgetBackendOptions({ MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE: 'off' }); + assert.equal(options.contextBudget?.staleToolResultPrune, undefined); + assert.deepEqual(options.contextBudget?.activeToolResultPrune, { enabled: true }); + }); + + test('Harbor stale tool result prune min recent turns falls back to MAKA_CONTEXT_MIN_RECENT_TURNS', () => { + const fallback = buildHarborCellContextBudgetBackendOptions({ MAKA_CONTEXT_MIN_RECENT_TURNS: '3' }); + assert.equal(fallback.contextBudget?.staleToolResultPrune?.minRecentTurnsFull, 3); + + const explicit = buildHarborCellContextBudgetBackendOptions({ + MAKA_CONTEXT_MIN_RECENT_TURNS: '3', + MAKA_CONTEXT_STALE_TOOL_RESULT_MIN_RECENT_TURNS: '5', + }); + assert.equal(explicit.contextBudget?.staleToolResultPrune?.minRecentTurnsFull, 5); + }); + + test('Harbor context budget is empty when both default-on prunes are explicitly off', () => { + const env = { + MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE: 'off', + MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE: 'off', + }; + assert.equal(buildHarborCellContextBudgetBackendOptions({ ...env }).contextBudget, undefined); + assert.equal(buildHarborCellContextBudgetPolicySnapshot({ ...env }), undefined); }); test('Harbor parses semantic compact policy for headless runs', () => { diff --git a/packages/headless/src/harbor-cell.ts b/packages/headless/src/harbor-cell.ts index 4809c3b51e..6c453b1c1d 100644 --- a/packages/headless/src/harbor-cell.ts +++ b/packages/headless/src/harbor-cell.ts @@ -704,7 +704,7 @@ export function buildHarborCellContextBudgetBackendOptions( env.MAKA_HARBOR_CONTEXT_STALE_TOOL_RESULT_PRUNE ?? env.MAKA_TOOL_RESULT_PRUNE, 'MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE', - ) ?? false; + ) ?? true; const activePruneEnabled = booleanEnv( env.MAKA_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE ?? env.MAKA_HARBOR_CONTEXT_ACTIVE_TOOL_RESULT_PRUNE ?? @@ -760,7 +760,9 @@ export function buildHarborCellContextBudgetBackendOptions( contextBudget.staleToolResultPrune = { enabled: true, ...(maxResultEstimatedTokens !== undefined ? { maxResultEstimatedTokens } : {}), - ...(minRecentTurnsFull !== undefined ? { minRecentTurnsFull } : {}), + // Explicit so the runtime protection window matches the policy snapshot + // and desktop default (2) instead of the runtime's internal ?? 1 fallback. + minRecentTurnsFull: minRecentTurnsFull ?? minRecentTurns ?? 2, }; } diff --git a/packages/runtime/src/context-budget-policy.ts b/packages/runtime/src/context-budget-policy.ts index b20b454cb4..ade6ab165a 100644 --- a/packages/runtime/src/context-budget-policy.ts +++ b/packages/runtime/src/context-budget-policy.ts @@ -95,7 +95,11 @@ export function buildManualCompactLookupPolicy( function buildStaleToolResultPrunePolicy( env: Record, ): NonNullable | undefined { - if (env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE !== 'on') return undefined; + const enabled = parseOptionalBoolean( + env.MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE, + 'MAKA_CONTEXT_STALE_TOOL_RESULT_PRUNE', + ); + if (enabled === false) return undefined; return { enabled: true, maxResultEstimatedTokens: parsePositiveInt(