dsh-context-compression-improved 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.gitattributes +1 -0
  2. package/.github/workflows/ci.yml +39 -0
  3. package/CHANGELOG.ja.md +39 -0
  4. package/CHANGELOG.ko.md +39 -0
  5. package/CHANGELOG.md +135 -0
  6. package/CHANGELOG.zh.md +39 -0
  7. package/CONTRIBUTING.md +22 -0
  8. package/README.ja.md +104 -0
  9. package/README.ko.md +103 -0
  10. package/README.md +89 -12
  11. package/README.zh.md +87 -12
  12. package/SECURITY.md +18 -0
  13. package/THIRD_PARTY_NOTICES.md +7 -31
  14. package/docs/installation.ja.md +76 -0
  15. package/docs/installation.ko.md +76 -0
  16. package/docs/installation.md +76 -0
  17. package/docs/installation.zh.md +76 -0
  18. package/docs/repair-log.md +582 -0
  19. package/eslint.config.js +30 -0
  20. package/package.json +84 -81
  21. package/packages/selector/LICENSE +21 -0
  22. package/packages/selector/README.md +26 -0
  23. package/packages/selector/README.zh.md +26 -0
  24. package/packages/selector/THIRD_PARTY_NOTICES.md +38 -0
  25. package/packages/selector/docs/history-tool-call-working-set-spec.md +112 -0
  26. package/packages/selector/docs/native-tool-result-selector-spec.md +34 -0
  27. package/packages/selector/docs/subagent-cache-reuse-spec.md +46 -0
  28. package/packages/selector/lib/style.css +308 -0
  29. package/packages/selector/package.json +115 -0
  30. package/{screenshots.json → packages/selector/screenshots.json} +6 -6
  31. package/packages/selector/src/client/CompressionProfileControls.tsx +229 -0
  32. package/packages/selector/src/client/CompressionProfileSelector.module.css +170 -0
  33. package/packages/selector/src/client/CompressionProfileSelector.tsx +79 -0
  34. package/packages/selector/src/client/CustomPolicyEditor.tsx +216 -0
  35. package/packages/selector/src/client/EstimatorControls.tsx +281 -0
  36. package/packages/selector/src/client/decode.ts +49 -0
  37. package/packages/selector/src/client/index.ts +111 -0
  38. package/packages/selector/src/client/locales.ts +198 -0
  39. package/packages/selector/src/client/preset-options.ts +70 -0
  40. package/packages/selector/src/client/settings-section.tsx +126 -0
  41. package/packages/selector/src/css-modules.d.ts +6 -0
  42. package/packages/selector/src/deepseek-v4-tokenizer.ts +210 -0
  43. package/packages/selector/src/estimator-catalog.ts +104 -0
  44. package/packages/selector/src/index.ts +327 -0
  45. package/packages/selector/src/invariant.ts +113 -0
  46. package/packages/selector/src/preset-overlay.ts +567 -0
  47. package/packages/selector/src/profiles.ts +342 -0
  48. package/packages/selector/src/pruner/content.ts +188 -0
  49. package/packages/selector/src/pruner/session.ts +94 -0
  50. package/packages/selector/src/pruner/state.ts +43 -0
  51. package/packages/selector/src/pruner/tuning.ts +23 -0
  52. package/packages/selector/src/pruner/types.ts +60 -0
  53. package/packages/selector/src/pruner.ts +2144 -0
  54. package/packages/selector/src/runtime/adaptive-cost.ts +194 -0
  55. package/packages/selector/src/runtime/audit.ts +215 -0
  56. package/packages/selector/src/runtime/config.ts +613 -0
  57. package/packages/selector/src/runtime/custom-policy.ts +278 -0
  58. package/packages/selector/src/runtime/deepseek-official-pricing.ts +298 -0
  59. package/packages/selector/src/runtime/deepseek-v4-vision-tokens.ts +254 -0
  60. package/packages/selector/src/runtime/measurement.ts +403 -0
  61. package/packages/selector/src/runtime/reducers.ts +656 -0
  62. package/packages/selector/src/runtime/retrieve.ts +457 -0
  63. package/packages/selector/src/runtime/session-events.ts +17 -0
  64. package/packages/selector/src/runtime/tail-trim.ts +166 -0
  65. package/packages/selector/src/runtime/token-count.ts +72 -0
  66. package/packages/selector/src/runtime/tokenpilot/dedup.ts +81 -0
  67. package/packages/selector/src/runtime/tokenpilot/estimator.ts +183 -0
  68. package/packages/selector/src/runtime/tokenpilot/locator.ts +128 -0
  69. package/packages/selector/src/runtime/tokenpilot/read-state.ts +77 -0
  70. package/packages/selector/src/runtime/types.ts +309 -0
  71. package/packages/selector/src/runtime/value.ts +48 -0
  72. package/packages/selector/tests/auto-compact.client.spec.tsx +226 -0
  73. package/packages/selector/tests/built/client-artifact.spec.ts +51 -0
  74. package/packages/selector/tests/cache-prefix-audit.spec.ts +123 -0
  75. package/packages/selector/tests/code-skeleton.client.spec.ts +88 -0
  76. package/packages/selector/tests/custom-contract.client.spec.ts +202 -0
  77. package/packages/selector/tests/estimator-catalog.spec.ts +70 -0
  78. package/packages/selector/tests/estimator-channel.client.spec.tsx +247 -0
  79. package/packages/selector/tests/estimator-route-registration.host.spec.ts +176 -0
  80. package/packages/selector/tests/host-preset-overlay.host.spec.ts +204 -0
  81. package/packages/selector/tests/preset-options-write.client.spec.ts +181 -0
  82. package/packages/selector/tests/preset-overlay-loader.e2e.host.spec.ts +196 -0
  83. package/packages/selector/tests/preset-overlay.host.spec.ts +243 -0
  84. package/packages/selector/tests/profiles.client.spec.tsx +434 -0
  85. package/packages/selector/tests/public/package-contract.client.spec.ts +33 -0
  86. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +167 -0
  87. package/packages/selector/tests/runtime/audit.spec.ts +129 -0
  88. package/packages/selector/tests/runtime/auto-compact-config.spec.ts +523 -0
  89. package/packages/selector/tests/runtime/code-skeleton.spec.ts +141 -0
  90. package/packages/selector/tests/runtime/deepseek-official-pricing.spec.ts +186 -0
  91. package/packages/selector/tests/runtime/deepseek-v4-tokenizer.spec.ts +122 -0
  92. package/packages/selector/tests/runtime/deepseek-v4-vision-tokens.spec.ts +122 -0
  93. package/packages/selector/tests/runtime/fixtures/profile-baseline.json +273 -0
  94. package/packages/selector/tests/runtime/fixtures/tokenizer-golden.json +106 -0
  95. package/packages/selector/tests/runtime/fixtures/vision-golden.json +459 -0
  96. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +2531 -0
  97. package/packages/selector/tests/runtime/session-events.spec.ts +27 -0
  98. package/packages/selector/tests/runtime/tokenizer-golden.spec.ts +53 -0
  99. package/packages/selector/tests/runtime/tokenpilot/dedup.spec.ts +52 -0
  100. package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +56 -0
  101. package/packages/selector/tests/runtime/tokenpilot/locator.spec.ts +76 -0
  102. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +100 -0
  103. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +58 -0
  104. package/packages/selector/tests/runtime/value.spec.ts +23 -0
  105. package/packages/selector/tests/standing-generation.host.spec.ts +631 -0
  106. package/packages/selector/tests/subagent-cache-reuse.host.spec.ts +250 -0
  107. package/packages/selector/tests/support/cache-prefix-audit.ts +105 -0
  108. package/packages/selector/tests/support/mock-adapter.ts +37 -0
  109. package/packages/selector/tests/support/ui-primitives.tsx +34 -0
  110. package/packages/selector/tsconfig.json +11 -0
  111. package/packages/selector/tsdown.client.config.ts +102 -0
  112. package/packages/selector/tsdown.config.ts +20 -0
  113. package/pnpm-workspace.yaml +19 -0
  114. package/scripts/capture-profile-baseline.ts +80 -0
  115. package/scripts/generate-tokenizer-fixtures.py +81 -0
  116. package/scripts/generate-vision-fixtures.py +208 -0
  117. package/scripts/packed-components-smoke.ts +713 -0
  118. package/scripts/packed-install-e2e.ts +1072 -0
  119. package/scripts/verify-release.ts +300 -0
  120. package/tests/TEST_INVENTORY.md +42 -0
  121. package/tsconfig.base.json +18 -0
  122. package/tsconfig.json +7 -0
  123. package/tsconfig.scripts.json +13 -0
  124. package/tsconfig.tests.json +15 -0
  125. package/vitest.built.config.ts +9 -0
  126. package/vitest.config.ts +43 -0
  127. /package/{assets → packages/selector/assets}/deepseek-v4/LICENSE.DeepSeek-V4-Pro.txt +0 -0
  128. /package/{assets → packages/selector/assets}/deepseek-v4/manifest.json +0 -0
  129. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer.json +0 -0
  130. /package/{assets → packages/selector/assets}/deepseek-v4/tokenizer_config.json +0 -0
  131. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/LICENSE.DeepSeek-V4-Flash-Vision-Exp.txt +0 -0
  132. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/manifest.json +0 -0
  133. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer.json +0 -0
  134. /package/{assets → packages/selector/assets}/deepseek-v4-vision-exp/tokenizer_config.json +0 -0
  135. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-profiles.jpg +0 -0
  136. /package/{assets → packages/selector/assets}/screenshots/context-compression-selector-settings.png +0 -0
  137. /package/{cordis.patch.yml → packages/selector/cordis.patch.yml} +0 -0
  138. /package/{dsh.plugin.json → packages/selector/dsh.plugin.json} +0 -0
  139. /package/{lib → packages/selector/lib}/client.d.ts +0 -0
  140. /package/{lib → packages/selector/lib}/client.js +0 -0
  141. /package/{lib → packages/selector/lib}/config.js +0 -0
  142. /package/{lib → packages/selector/lib}/index.d.ts +0 -0
  143. /package/{lib → packages/selector/lib}/index.js +0 -0
  144. /package/{lib → packages/selector/lib}/invariant.d.ts +0 -0
  145. /package/{lib → packages/selector/lib}/invariant.js +0 -0
  146. /package/{lib → packages/selector/lib}/pruner.d.ts +0 -0
  147. /package/{lib → packages/selector/lib}/pruner.js +0 -0
  148. /package/{lib → packages/selector/lib}/tail-trim.js +0 -0
@@ -0,0 +1,2531 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { Context } from '@deepseek-ai/cordis'
3
+ import {
4
+ CallId,
5
+ createMessage,
6
+ createUserMessage,
7
+ createToolResultMessage,
8
+ LlmAdapter,
9
+ type ContentBlock,
10
+ type GenerateOptions,
11
+ type LlmResolvedModelInfo,
12
+ type StreamChunk,
13
+ } from '@deepseek-ai/dsh-llm'
14
+ import LlmRuntime from '@deepseek-ai/dsh-llm'
15
+ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
16
+ import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
17
+ import BasicCompactionEngine from '@deepseek-ai/dsh-compaction-basic'
18
+ import SessionStore, {
19
+ Session,
20
+ SessionId,
21
+ canonicalHeader,
22
+ } from '@deepseek-ai/dsh-session'
23
+ import {
24
+ agentEvents,
25
+ Inbox,
26
+ type Agent,
27
+ } from '@deepseek-ai/dsh-agent'
28
+ import {
29
+ SettingsProvider,
30
+ settingsNamespace,
31
+ type SettingsNamespace,
32
+ } from '@deepseek-ai/dsh-settings'
33
+ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
34
+ import TokenMeter from '@deepseek-ai/dsh-token-meter'
35
+ import ToolRuntime from '@deepseek-ai/dsh-tools'
36
+ import { CompactionId } from '@deepseek-ai/dsh-compaction'
37
+ import InvariantRegistry from '@deepseek-ai/dsh-invariants'
38
+ import * as SelectorHost from '../../../src/index.ts'
39
+ import * as RuntimeInvariant from '../../../src/invariant.ts'
40
+ import ToolResultPruner, {
41
+ CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
42
+ DEFAULT_CUSTOM_COMPRESSION_POLICY,
43
+ resolveConfig,
44
+ resolvePolicy,
45
+ } from '../../../src/pruner.ts'
46
+ import type { CustomCompressionPolicy } from '../../../src/pruner.ts'
47
+ import { DEEPSEEK_V4_TOKENIZER_ARTIFACT, deepSeekV4TokenizerForModel } from '../../../src/deepseek-v4-tokenizer.ts'
48
+ import {
49
+ DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
50
+ DEEPSEEK_VISION_IMAGE_ESTIMATOR,
51
+ DEEPSEEK_VISION_PROJECTION,
52
+ deepSeekVisionImageBlockTokens,
53
+ deepSeekVisionImageGrid,
54
+ } from '../../../src/runtime/deepseek-v4-vision-tokens.ts'
55
+ import { validatePublishedTailTrim } from '../../../src/runtime/tail-trim.ts'
56
+ import { measureForCompaction } from '../../../src/runtime/measurement.ts'
57
+ import {
58
+ COMPRESSION_AUDIT_PREFIX,
59
+ type CompressionAuditRecord,
60
+ type CompressionRewriteAuditRecord,
61
+ } from '../../../src/runtime/audit.ts'
62
+
63
+ const MODEL = 'deepseek-v4-flash'
64
+ const VISION_MODEL = 'deepseek-v4-flash-vision-exp'
65
+ const activeContexts: Context[] = []
66
+
67
+ /** Metadata-only raster image block; never carries image bytes. */
68
+ function imageBlock(width: number, height: number): ContentBlock {
69
+ // AttachmentId is an opaque brand over string; tests construct the durable
70
+ // reference structurally without importing the attachment package.
71
+ return {
72
+ type: 'image',
73
+ attachment: {
74
+ attachmentId: `test-image-${String(width)}x${String(height)}`,
75
+ mediaType: 'image/png',
76
+ bytes: 1_024,
77
+ width,
78
+ height,
79
+ },
80
+ } as ContentBlock
81
+ }
82
+
83
+ afterEach(async () => {
84
+ for (const ctx of activeContexts.splice(0)) await ctx.fiber.dispose()
85
+ })
86
+
87
+ class TestSettings extends SettingsProvider {
88
+ readonly writable = true
89
+ private readonly stored: Record<string, unknown> = {}
90
+
91
+ protected override load(): Promise<Record<string, unknown>> {
92
+ return Promise.resolve(structuredClone(this.stored))
93
+ }
94
+
95
+ protected override persist(namespace: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
96
+ this.stored[namespace] = structuredClone(section)
97
+ return Promise.resolve()
98
+ }
99
+ }
100
+
101
+ class NativeSummaryAdapter extends LlmAdapter {
102
+ constructor(
103
+ private readonly responses: string[],
104
+ private readonly contextWindow: number,
105
+ ) {
106
+ super()
107
+ }
108
+
109
+ override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
110
+ return Promise.resolve({
111
+ provider,
112
+ id: model,
113
+ name: model,
114
+ context: { contextWindow: this.contextWindow },
115
+ })
116
+ }
117
+
118
+ override async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
119
+ options.signal?.throwIfAborted()
120
+ const text = this.responses.shift()
121
+ if (text === undefined) throw new Error('NativeSummaryAdapter: response script exhausted')
122
+ yield { type: 'block-start', index: 0, blockType: 'text' }
123
+ yield { type: 'text-delta', index: 0, text }
124
+ yield { type: 'block-end', index: 0, block: { type: 'text', text } }
125
+ yield { type: 'usage', usage: { inputTokens: 10, outputTokens: text.length } }
126
+ yield { type: 'finish', reason: { kind: 'stop' } }
127
+ }
128
+ }
129
+
130
+ async function runtimeContext(): Promise<Context> {
131
+ const ctx = new Context()
132
+ activeContexts.push(ctx)
133
+ await ctx.plugin(SessionStore)
134
+ await ctx.plugin(SystemPrompt)
135
+ await ctx.plugin(ToolRuntime)
136
+ await ctx.plugin(TokenMeter)
137
+ return ctx
138
+ }
139
+
140
+ function captureAudit(ctx: Context): {
141
+ records(): CompressionAuditRecord[]
142
+ } {
143
+ const info = vi.spyOn(ctx.logger, 'info').mockImplementation(() => ctx.logger)
144
+ return {
145
+ records: () => info.mock.calls.flatMap((call) => {
146
+ const line = String(call[0])
147
+ return line.startsWith(COMPRESSION_AUDIT_PREFIX)
148
+ ? [JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord]
149
+ : []
150
+ }),
151
+ }
152
+ }
153
+
154
+ function rewrites(records: readonly CompressionAuditRecord[]): CompressionRewriteAuditRecord[] {
155
+ return records.filter((record): record is CompressionRewriteAuditRecord => record.kind === 'rewrite')
156
+ }
157
+
158
+ function appendToolTurn(
159
+ session: Session,
160
+ turn: number,
161
+ text: string,
162
+ closeTurn: boolean,
163
+ userText?: string,
164
+ provider = 'deepseek',
165
+ model: string = MODEL,
166
+ ): { readonly assistantSeq: number; readonly resultSeq: number } {
167
+ const callId = CallId(`call-${String(turn)}`)
168
+ session.append('turn/start', { turn })
169
+ if (session.requestHeader() === undefined) {
170
+ session.append('request/header', {
171
+ reason: 'initial',
172
+ header: canonicalHeader({ config: { provider, model } }),
173
+ })
174
+ }
175
+ if (userText !== undefined) {
176
+ session.append('user/message', createUserMessage({
177
+ content: [{ type: 'text', text: userText }],
178
+ source: { kind: 'user' },
179
+ }), { surfaceOp: 'append' })
180
+ }
181
+ session.append('step/start', { turn, step: 1 })
182
+ const assistant = session.append('assistant/message', {
183
+ turn,
184
+ step: 1,
185
+ message: createMessage({
186
+ role: 'assistant',
187
+ content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
188
+ source: { kind: 'model', provider, model },
189
+ }),
190
+ }, { surfaceOp: 'append' })
191
+ session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' })
192
+ const result = session.append('tool/result', {
193
+ turn,
194
+ step: 1,
195
+ message: createToolResultMessage({
196
+ callId,
197
+ content: [{ type: 'text', text }],
198
+ isError: false,
199
+ }),
200
+ }, { surfaceOp: 'append' })
201
+ session.append('step/end', { turn, step: 1 })
202
+ if (closeTurn) session.append('turn/end', { turn, reason: { kind: 'completed' } })
203
+ return { assistantSeq: assistant.seq, resultSeq: result.seq }
204
+ }
205
+
206
+ function appendToolBatchTurn(
207
+ session: Session,
208
+ turn: number,
209
+ texts: readonly string[],
210
+ closeTurn: boolean,
211
+ userText?: string,
212
+ ): { readonly assistantSeq: number; readonly resultSeqs: readonly number[] } {
213
+ const calls = texts.map((_, index) => ({
214
+ id: CallId(`call-${String(turn)}-${String(index + 1)}`),
215
+ name: 'bash',
216
+ }))
217
+ session.append('turn/start', { turn })
218
+ if (session.requestHeader() === undefined) {
219
+ session.append('request/header', {
220
+ reason: 'initial',
221
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
222
+ })
223
+ }
224
+ if (userText !== undefined) {
225
+ session.append('user/message', createUserMessage({
226
+ content: [{ type: 'text', text: userText }],
227
+ source: { kind: 'user' },
228
+ }), { surfaceOp: 'append' })
229
+ }
230
+ session.append('step/start', { turn, step: 1 })
231
+ const assistant = session.append('assistant/message', {
232
+ turn,
233
+ step: 1,
234
+ message: createMessage({
235
+ role: 'assistant',
236
+ content: calls.map(call => ({
237
+ type: 'tool-call' as const,
238
+ id: call.id,
239
+ name: call.name,
240
+ arguments: '{}',
241
+ })),
242
+ source: { kind: 'model', provider: 'deepseek', model: MODEL },
243
+ }),
244
+ }, { surfaceOp: 'append' })
245
+ const resultSeqs: number[] = []
246
+ for (const [index, call] of calls.entries()) {
247
+ session.append('tool/call', {
248
+ turn,
249
+ step: 1,
250
+ callId: call.id,
251
+ name: call.name,
252
+ arguments: '{}',
253
+ })
254
+ const result = session.append('tool/result', {
255
+ turn,
256
+ step: 1,
257
+ message: createToolResultMessage({
258
+ callId: call.id,
259
+ content: [{ type: 'text', text: texts[index] ?? '' }],
260
+ isError: false,
261
+ }),
262
+ }, { surfaceOp: 'append' })
263
+ resultSeqs.push(result.seq)
264
+ }
265
+ session.append('step/end', { turn, step: 1 })
266
+ if (closeTurn) session.append('turn/end', { turn, reason: { kind: 'completed' } })
267
+ return { assistantSeq: assistant.seq, resultSeqs }
268
+ }
269
+
270
+ function stubAgent(ctx: Context, session: Session): Agent {
271
+ return {
272
+ id: session.id,
273
+ options: {},
274
+ session,
275
+ inbox: new Inbox(session, { inserted: () => {}, discarded: () => {}, claimed: () => {} }),
276
+ status: 'idle',
277
+ ctx,
278
+ send: () => {},
279
+ followup: () => {},
280
+ steer: () => ({ outcome: Promise.resolve({ status: 'rejected' as const }) }),
281
+ inject: () => {},
282
+ cancel() {},
283
+ runMaintenance: task => task(new AbortController().signal),
284
+ whenIdle: () => Promise.resolve(),
285
+ }
286
+ }
287
+
288
+ describe('standalone runtime on published Harness APIs', () => {
289
+ it('loads the pinned official tokenizers and refuses unverified model ids', () => {
290
+ const tokenizer = deepSeekV4TokenizerForModel(MODEL)
291
+ expect(tokenizer?.countText('DeepSeek Harness').tokens).toBeGreaterThan(0)
292
+ expect(deepSeekV4TokenizerForModel('deepseek-v4-flash-vision-exp')?.countText('DeepSeek Harness')).toMatchObject({
293
+ kind: 'exact-tokenizer',
294
+ tokenizerId: 'deepseek-ai/DeepSeek-V4-Flash-Vision-Exp',
295
+ tokenizerRevision: '6821d6ad3681a4b137b066b76094fa82ebd0a380',
296
+ })
297
+ expect(deepSeekV4TokenizerForModel('deepseek-v4-flash-vision')).toBeUndefined()
298
+ })
299
+
300
+ it.each([
301
+ ['off', Number.MAX_SAFE_INTEGER],
302
+ ['native', Number.MAX_SAFE_INTEGER],
303
+ ['balanced', 500_000],
304
+ ['cache-strict', 600_000],
305
+ ['savings', 400_000],
306
+ ['adaptive', 500_000],
307
+ ] as const)('resolves %s with the shared 10-call and 64k token-tail History working set', (profile, trigger) => {
308
+ const policy = resolvePolicy(resolveConfig(), profile)
309
+
310
+ expect(policy.historyTriggerTokens).toBe(trigger)
311
+ expect(policy.historyKeepRecentToolCalls).toBe(10)
312
+ expect(policy.historyKeepRecentTokens).toBe(64_000)
313
+ })
314
+
315
+ it.each([
316
+ 'balanced',
317
+ 'cache-strict',
318
+ 'savings',
319
+ 'adaptive',
320
+ 'custom',
321
+ ] as const)('disables native head/middle/tail pruning for %s', (profile) => {
322
+ expect(resolvePolicy(resolveConfig(), profile).nativeToolResultEnabled).toBe(false)
323
+ })
324
+
325
+ it('enables native head/middle/tail pruning only for the Native profile', () => {
326
+ expect(resolvePolicy(resolveConfig(), 'native').nativeToolResultEnabled).toBe(true)
327
+ expect(resolvePolicy(resolveConfig(), 'off').nativeToolResultEnabled).toBe(false)
328
+ })
329
+
330
+ it('lands a standard compaction/prune plus tool-result replacement without custom events', async () => {
331
+ const ctx = await runtimeContext()
332
+ const audit = captureAudit(ctx)
333
+ await ctx.plugin(ToolResultPruner, {
334
+ profile: 'native',
335
+ nativeTriggerTokens: 100,
336
+ nativeTargetTokens: 64,
337
+ headChars: 8,
338
+ tailChars: 8,
339
+ }).await()
340
+ const pruner = ctx.toolResultPruner
341
+ const session = Session.create(SessionId('public-native-prune'))
342
+ const source = appendToolTurn(session, 1, 'x'.repeat(8_000), false)
343
+ const view = measureForCompaction(ctx, session)
344
+ expect(ctx.tools.get('context_compression_retrieve')).toBeDefined()
345
+ expect(view.currentSurface.kind).toBe('exact-tokenizer')
346
+ if (view.currentSurface.kind === 'exact-tokenizer') {
347
+ expect(view.currentSurface.tokens).toBeGreaterThan(100)
348
+ }
349
+
350
+ const result = pruner.pruneSession(session, { stage: 'pressure' })
351
+
352
+ expect(result.pruned).toHaveLength(1)
353
+ const manifest = session.events.at(-2)
354
+ const replacement = session.events.at(-1)
355
+ expect(manifest?.type).toBe('compaction/prune')
356
+ expect(session.events.some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
357
+ expect(replacement?.type).toBe('tool/result')
358
+ if (replacement?.type !== 'tool/result') throw new Error('Native prune did not append a tool result replacement')
359
+ expect(replacement.surfaceOp).toEqual({
360
+ op: 'replace',
361
+ start: source.resultSeq,
362
+ end: source.resultSeq,
363
+ })
364
+ expect(replacement.sourceEventSeqs).toContain(source.resultSeq)
365
+ expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
366
+ component: 'native-tool-result',
367
+ stage: 'pressure',
368
+ manifestSeq: manifest?.seq,
369
+ replacementSeq: replacement?.seq,
370
+ tokensBefore: expect.any(Number),
371
+ tokensAfter: expect.any(Number),
372
+ }))
373
+ })
374
+
375
+ it('proves isolated Fresh with exact before/after audit evidence', async () => {
376
+ const ctx = await runtimeContext()
377
+ const audit = captureAudit(ctx)
378
+ await ctx.plugin(ToolResultPruner, {
379
+ profile: 'balanced',
380
+ freshTriggerTokens: 100,
381
+ freshTargetTokens: 64,
382
+ aggregateTriggerTokens: 100_000,
383
+ aggregateTargetTokens: 90_000,
384
+ historyTriggerTokens: 100_000,
385
+ }).await()
386
+ const session = Session.create(SessionId('public-fresh-e2e'))
387
+ appendToolTurn(session, 1, 'fresh evidence '.repeat(1_000), false, undefined, 'deepseek-official')
388
+
389
+ const result = ctx.toolResultPruner.pruneSession(session, {
390
+ stage: 'fresh',
391
+ freshTurn: 1,
392
+ freshStep: 1,
393
+ })
394
+
395
+ expect(result.pruned).toHaveLength(1)
396
+ const record = rewrites(audit.records()).find(entry => entry.component === 'fresh')
397
+ expect(record).toMatchObject({
398
+ stage: 'fresh',
399
+ reducer: expect.any(String),
400
+ manifestEventType: 'compaction/prune',
401
+ tokenizerId: expect.any(String),
402
+ tokenizerRevision: expect.any(String),
403
+ })
404
+ expect(record?.tokensBefore).toBeGreaterThan(100)
405
+ expect(record?.tokensAfter).toBeLessThanOrEqual(64)
406
+ expect(record?.tokensRemoved).toBe((record?.tokensBefore ?? 0) - (record?.tokensAfter ?? 0))
407
+ })
408
+
409
+ it('proves isolated Aggregate while Fresh and History remain below their gates', async () => {
410
+ const ctx = await runtimeContext()
411
+ const audit = captureAudit(ctx)
412
+ await ctx.plugin(ToolResultPruner, {
413
+ profile: 'balanced',
414
+ freshTriggerTokens: 100_000,
415
+ freshTargetTokens: 90_000,
416
+ aggregateTriggerTokens: 100,
417
+ aggregateTargetTokens: 64,
418
+ historyTriggerTokens: 100_000,
419
+ }).await()
420
+ const session = Session.create(SessionId('public-aggregate-e2e'))
421
+ appendToolTurn(session, 1, 'aggregate evidence '.repeat(1_000), false, undefined, 'deepseek-official')
422
+
423
+ const result = ctx.toolResultPruner.pruneSession(session, {
424
+ stage: 'fresh',
425
+ freshTurn: 1,
426
+ freshStep: 1,
427
+ })
428
+
429
+ expect(result.pruned).toHaveLength(1)
430
+ const record = rewrites(audit.records()).find(entry => entry.component === 'aggregate')
431
+ expect(record).toMatchObject({
432
+ stage: 'fresh',
433
+ reducer: 'fresh-step-aggregate',
434
+ manifestEventType: 'compaction/prune',
435
+ })
436
+ expect(record?.tokensBefore).toBeGreaterThan(100)
437
+ expect(record?.tokensAfter).toBeLessThanOrEqual(64)
438
+ expect(rewrites(audit.records()).some(entry => entry.component === 'fresh')).toBe(false)
439
+ expect(rewrites(audit.records()).some(entry => entry.component === 'history')).toBe(false)
440
+ })
441
+
442
+ it('ages only results outside the recent tool-call and token-tail working set', async () => {
443
+ const ctx = await runtimeContext()
444
+ const session = Session.create(SessionId('public-history-tool-call-working-set'))
445
+ const batch = appendToolBatchTurn(
446
+ session,
447
+ 1,
448
+ Array.from({ length: 5 }, () => 'working-set evidence '.repeat(1_000)),
449
+ true,
450
+ )
451
+ session.append('turn/start', { turn: 2 })
452
+ const view = measureForCompaction(ctx, session)
453
+ const counts = batch.resultSeqs.map((seq) => {
454
+ const entry = view.measuredNodes.find(node => node.seq === seq)?.count
455
+ if (entry?.kind !== 'exact-tokenizer') throw new Error('working-set test needs exact result counts')
456
+ return entry.tokens
457
+ })
458
+ const recentTailTokens = counts.slice(-3).reduce((sum, tokens) => sum + tokens, 0) - 1
459
+ const totalToolTokens = counts.reduce((sum, tokens) => sum + tokens, 0)
460
+
461
+ await ctx.plugin(ToolResultPruner, {
462
+ profile: 'balanced',
463
+ freshTriggerTokens: 100_000,
464
+ freshTargetTokens: 90_000,
465
+ aggregateTriggerTokens: 100_000,
466
+ aggregateTargetTokens: 90_000,
467
+ // The strict required-reclaim gate demands one batch pull tool tokens
468
+ // back below the trigger; 72% leaves exactly the two oldest results as
469
+ // the reclaimable margin above the placeholder residue.
470
+ historyTriggerTokens: Math.floor(totalToolTokens * 0.72),
471
+ historyKeepRecentToolCalls: 2,
472
+ historyKeepRecentTokens: recentTailTokens,
473
+ historyMinReclaimTokens: 1,
474
+ }).await()
475
+
476
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
477
+
478
+ expect(result.pruned).toHaveLength(2)
479
+ expect(result.pruned.map(entry => entry.originalSeq)).toEqual(batch.resultSeqs.slice(0, 2))
480
+ expect(result.pruned.map(entry => entry.originalSeq)).not.toContain(batch.resultSeqs.at(-1))
481
+ expect(result.pruned.map(entry => entry.originalSeq)).not.toContain(batch.resultSeqs.at(-2))
482
+ expect(result.pruned.map(entry => entry.originalSeq)).not.toContain(batch.resultSeqs.at(-3))
483
+ })
484
+
485
+ it('proves isolated routine History against completed old turns', async () => {
486
+ const ctx = await runtimeContext()
487
+ const audit = captureAudit(ctx)
488
+ await ctx.plugin(ToolResultPruner, {
489
+ profile: 'savings',
490
+ freshTriggerTokens: 100_000,
491
+ freshTargetTokens: 90_000,
492
+ aggregateTriggerTokens: 100_000,
493
+ aggregateTargetTokens: 90_000,
494
+ historyTriggerTokens: 1_200,
495
+ historyKeepRecentToolCalls: 0,
496
+ historyKeepRecentTokens: 1,
497
+ historyMinReclaimTokens: 1,
498
+ }).await()
499
+ const session = Session.create(SessionId('public-history-e2e'))
500
+ appendToolTurn(session, 1, 'old historical evidence '.repeat(1_000), true)
501
+ // The one-token recent working-set budget protects at least the newest
502
+ // completed result, so provide a second completed turn and age the oldest.
503
+ appendToolTurn(session, 2, 'newer protected evidence', true)
504
+ session.append('turn/start', { turn: 3 })
505
+
506
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
507
+
508
+ expect(result.pruned).toHaveLength(1)
509
+ const record = rewrites(audit.records()).find(entry => entry.component === 'history')
510
+ expect(record).toMatchObject({
511
+ stage: 'pressure',
512
+ historyMode: 'routine',
513
+ manifestEventType: 'compaction/prune',
514
+ })
515
+ expect(record?.tokensBefore).toBeGreaterThan(100)
516
+ expect(record?.tokensAfter).toBeLessThan(record?.tokensBefore ?? 0)
517
+ })
518
+
519
+ it('moves the capacity-pressure gate with the frozen Auto Compact threshold', async () => {
520
+ const ctx = await runtimeContext()
521
+ await ctx.plugin(TestSettings).await()
522
+ await ctx.plugin(SelectorHost).await()
523
+ const audit = captureAudit(ctx)
524
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
525
+
526
+ // A high threshold delays the micro-compact last-chance gate past the old
527
+ // fixed 0.7 ratio: pressure between 0.7*C and D must NOT age history.
528
+ await ctx.settings.update(namespace, { profile: 'cache-strict', autoCompact: { thresholdPercent: 90 } })
529
+ await ctx.plugin(ToolResultPruner, {
530
+ profile: 'cache-strict',
531
+ freshTriggerTokens: 100_000,
532
+ freshTargetTokens: 90_000,
533
+ aggregateTriggerTokens: 100_000,
534
+ aggregateTargetTokens: 90_000,
535
+ }).await()
536
+ const delayed = Session.create(SessionId('public-autocompact-90-delayed'))
537
+ appendToolTurn(delayed, 1, 'delayed gate evidence '.repeat(600), true)
538
+ appendToolTurn(delayed, 2, 'newest protected result', true)
539
+ const delayedTotal = measureForCompaction(ctx, delayed).totalTokens
540
+ const delayedWindow = Math.floor(delayedTotal / 0.72)
541
+ delayed.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: delayedWindow })
542
+ delayed.append('turn/start', { turn: 3 })
543
+ ctx.toolResultPruner.pruneSession(delayed, { stage: 'pressure' })
544
+ expect(rewrites(audit.records()).some(record =>
545
+ record.sessionId === String(delayed.id) && record.component === 'history')).toBe(false)
546
+ expect(audit.records()).toContainEqual(expect.objectContaining({
547
+ kind: 'component-evaluation',
548
+ sessionId: String(delayed.id),
549
+ component: 'history',
550
+ reason: 'below-micro-deadline',
551
+ triggerTokens: Math.floor(Math.floor(delayedWindow * 90 / 100) * 0.875),
552
+ }))
553
+ })
554
+
555
+ it('ages History earlier when the frozen Auto Compact threshold is low', async () => {
556
+ const ctx = await runtimeContext()
557
+ await ctx.plugin(TestSettings).await()
558
+ await ctx.plugin(SelectorHost).await()
559
+ const audit = captureAudit(ctx)
560
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
561
+
562
+ // A low threshold pulls the micro-compact deadline below the old fixed
563
+ // 0.7 ratio: pressure between D and 0.7*C must age history now.
564
+ await ctx.settings.update(namespace, { profile: 'cache-strict', autoCompact: { thresholdPercent: 50 } })
565
+ await ctx.plugin(ToolResultPruner, {
566
+ profile: 'cache-strict',
567
+ freshTriggerTokens: 100_000,
568
+ freshTargetTokens: 90_000,
569
+ aggregateTriggerTokens: 100_000,
570
+ aggregateTargetTokens: 90_000,
571
+ historyTriggerTokens: 400,
572
+ historyKeepRecentToolCalls: 0,
573
+ historyKeepRecentTokens: 1,
574
+ historyMinReclaimTokens: 1,
575
+ }).await()
576
+ const early = Session.create(SessionId('public-autocompact-50-early'))
577
+ appendToolTurn(early, 1, 'early gate evidence '.repeat(600), true)
578
+ appendToolTurn(early, 2, 'newest protected result', true)
579
+ const earlyTotal = measureForCompaction(ctx, early).totalTokens
580
+ const earlyWindow = Math.floor(earlyTotal / 0.6)
581
+ early.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: earlyWindow })
582
+ early.append('turn/start', { turn: 3 })
583
+ ctx.toolResultPruner.pruneSession(early, { stage: 'pressure' })
584
+
585
+ expect(rewrites(audit.records()).some(record =>
586
+ record.sessionId === String(early.id) && record.component === 'history')).toBe(true)
587
+ })
588
+
589
+ it('freezes the generation-owned threshold over later global settings changes', async () => {
590
+ const ctx = await runtimeContext()
591
+ await ctx.plugin(TestSettings).await()
592
+ await ctx.plugin(SelectorHost).await()
593
+ const audit = captureAudit(ctx)
594
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
595
+ // The preset overlay captured 70% into this generation's deployment
596
+ // config; the user then moved the global setting to 90 before the first
597
+ // prune. Auto Compact and micro compact must both stay on 70%.
598
+ await ctx.plugin(ToolResultPruner, {
599
+ profile: 'balanced',
600
+ autoCompactThresholdPercent: 70,
601
+ }).await()
602
+ const session = Session.create(SessionId('public-generation-owned-threshold'))
603
+ appendToolTurn(session, 1, 'generation-owned threshold evidence', false)
604
+ session.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
605
+ await ctx.settings.update(namespace, { profile: 'balanced', autoCompact: { thresholdPercent: 90 } })
606
+ ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
607
+
608
+ const resolved = audit.records().find(record =>
609
+ record.kind === 'policy-resolved' && record.sessionId === String(session.id))
610
+ expect(resolved).toMatchObject({
611
+ coordination: {
612
+ thresholdPercent: 70,
613
+ autoCompactTokens: 700_000,
614
+ microDeadlineTokens: 612_500,
615
+ },
616
+ })
617
+ const frozen = audit.records().find(record =>
618
+ record.kind === 'policy-frozen' && record.sessionId === String(session.id))
619
+ expect(frozen).toMatchObject({
620
+ autoCompactThresholdSource: 'generation-config',
621
+ settings: { autoCompact: { thresholdPercent: 70 } },
622
+ })
623
+ })
624
+
625
+ it('fails open to a lossless frozen policy when stored settings are malformed', async () => {
626
+ const ctx = await runtimeContext()
627
+ await ctx.plugin(TestSettings).await()
628
+ await ctx.plugin(SelectorHost).await()
629
+ const audit = captureAudit(ctx)
630
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
631
+ // A hand-edited store can surface a document the schema rejects (unknown
632
+ // top-level key). The runtime must not fall back to a lossy-capable
633
+ // profile: the session freezes effectively off and keeps every original
634
+ // tool result.
635
+ const malformed = {
636
+ profile: 'off',
637
+ custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
638
+ unrelated: true,
639
+ }
640
+ const originalGet = ctx.settings.get.bind(ctx.settings)
641
+ vi.spyOn(ctx.settings, 'get').mockImplementation((ns: unknown) =>
642
+ ns === undefined || String(ns) === String(namespace) ? structuredClone(malformed) : originalGet(ns as never))
643
+ await ctx.plugin(ToolResultPruner, {
644
+ profile: 'balanced',
645
+ freshTriggerTokens: 10,
646
+ freshTargetTokens: 8,
647
+ }).await()
648
+ const session = Session.create(SessionId('public-malformed-settings-fail-open'))
649
+ appendToolTurn(session, 1, 'malformed settings must not compress '.repeat(300), true)
650
+ session.append('turn/start', { turn: 2 })
651
+
652
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
653
+
654
+ expect(result.pruned).toHaveLength(0)
655
+ expect(rewrites(audit.records())).toHaveLength(0)
656
+ const frozen = audit.records().find(record =>
657
+ record.kind === 'policy-frozen' && record.sessionId === String(session.id))
658
+ expect(frozen).toMatchObject({
659
+ settingsInvalidFallback: 'lossless-off',
660
+ settings: { profile: 'off' },
661
+ })
662
+ })
663
+
664
+ it.each([
665
+ ['null profile', { profile: null, custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) }],
666
+ ['null custom', { profile: 'balanced', custom: null }],
667
+ ['both sections null', { profile: null, custom: null }],
668
+ ['own-property undefined profile', { profile: undefined, custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) }],
669
+ ['own-property undefined custom', { profile: 'off', custom: undefined }],
670
+ ['profile off plus malformed autoCompact', {
671
+ profile: 'off',
672
+ custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
673
+ autoCompact: { thresholdPercent: 400 },
674
+ }],
675
+ ])('fails open to a lossless frozen policy when the stored document carries %s', async (label, malformed) => {
676
+ const ctx = await runtimeContext()
677
+ await ctx.plugin(TestSettings).await()
678
+ await ctx.plugin(SelectorHost).await()
679
+ const audit = captureAudit(ctx)
680
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
681
+ // Schemastery `.default(...)` would silently replace these present-but-null
682
+ // sections with the balanced/default-v3 policy; the runtime must reject the
683
+ // document and freeze the session losslessly instead.
684
+ const originalGet = ctx.settings.get.bind(ctx.settings)
685
+ vi.spyOn(ctx.settings, 'get').mockImplementation((ns: unknown) =>
686
+ ns === undefined || String(ns) === String(namespace) ? structuredClone(malformed) : originalGet(ns as never))
687
+ await ctx.plugin(ToolResultPruner, {
688
+ profile: 'balanced',
689
+ freshTriggerTokens: 10,
690
+ freshTargetTokens: 8,
691
+ }).await()
692
+ const session = Session.create(SessionId(`public-null-settings-${label.replace(/\s+/gu, '-')}`))
693
+ appendToolTurn(session, 1, 'null settings must not compress '.repeat(300), true)
694
+ session.append('turn/start', { turn: 2 })
695
+
696
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
697
+
698
+ expect(result.pruned).toHaveLength(0)
699
+ expect(rewrites(audit.records())).toHaveLength(0)
700
+ const frozen = audit.records().find(record =>
701
+ record.kind === 'policy-frozen' && record.sessionId === String(session.id))
702
+ expect(frozen).toMatchObject({
703
+ settingsInvalidFallback: 'lossless-off',
704
+ settings: { profile: 'off' },
705
+ })
706
+ })
707
+
708
+ it('validates exotic Host settings before any clone can erase their prototype', async () => {
709
+ class AutoCompactDocument {
710
+ thresholdPercent = 73
711
+ }
712
+
713
+ const ctx = await runtimeContext()
714
+ await ctx.plugin(TestSettings).await()
715
+ await ctx.plugin(SelectorHost).await()
716
+ const audit = captureAudit(ctx)
717
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
718
+ const malformed = {
719
+ profile: 'balanced' as const,
720
+ custom: structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY),
721
+ autoCompact: new AutoCompactDocument(),
722
+ }
723
+ const originalGet = ctx.settings.get.bind(ctx.settings)
724
+ vi.spyOn(ctx.settings, 'get').mockImplementation((ns: unknown) =>
725
+ ns === undefined || String(ns) === String(namespace) ? malformed : originalGet(ns as never))
726
+ await ctx.plugin(ToolResultPruner, {
727
+ profile: 'balanced',
728
+ freshTriggerTokens: 10,
729
+ freshTargetTokens: 8,
730
+ }).await()
731
+ const session = Session.create(SessionId('public-exotic-settings-before-clone'))
732
+ appendToolTurn(session, 1, 'exotic settings must not compress '.repeat(300), false)
733
+
734
+ const result = ctx.toolResultPruner.pruneSession(session, {
735
+ stage: 'fresh',
736
+ freshTurn: 1,
737
+ freshStep: 1,
738
+ })
739
+
740
+ expect(result.pruned).toHaveLength(0)
741
+ expect(rewrites(audit.records())).toHaveLength(0)
742
+ expect(audit.records()).toContainEqual(expect.objectContaining({
743
+ kind: 'policy-frozen',
744
+ sessionId: String(session.id),
745
+ settingsInvalidFallback: 'lossless-off',
746
+ settings: expect.objectContaining({ profile: 'off' }),
747
+ }))
748
+ })
749
+
750
+ it('emits a policy audit again when the route returns to a previous value', async () => {
751
+ const ctx = await runtimeContext()
752
+ const audit = captureAudit(ctx)
753
+ await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
754
+ const session = Session.create(SessionId('public-policy-audit-reroute-aba'))
755
+ const route = (model: string) => session.append('request/header', {
756
+ reason: 'change',
757
+ header: canonicalHeader({ config: { provider: 'deepseek', model } }),
758
+ })
759
+ const prune = (turn: number) => {
760
+ appendToolTurn(session, turn, `route evidence ${String(turn)}`, false)
761
+ ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: turn, freshStep: 1 })
762
+ }
763
+ prune(1)
764
+ route('deepseek-v4-pro')
765
+ prune(2)
766
+ route(MODEL)
767
+ prune(3)
768
+
769
+ const resolved = audit.records().filter((record): record is Extract<CompressionAuditRecord, { kind: 'policy-resolved' }> =>
770
+ record.kind === 'policy-resolved' && record.sessionId === String(session.id))
771
+ expect(resolved).toHaveLength(3)
772
+ expect(resolved.map(record => record.route?.model)).toEqual([MODEL, 'deepseek-v4-pro', MODEL])
773
+ })
774
+
775
+ it('reports no bundled tokenizer for a DeepSeek model behind another provider', async () => {
776
+ const ctx = await runtimeContext()
777
+ const audit = captureAudit(ctx)
778
+ await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
779
+ const session = Session.create(SessionId('public-policy-audit-foreign-provider'))
780
+ appendToolTurn(session, 1, 'foreign provider evidence', false, undefined, 'openai', 'deepseek-v4-pro')
781
+ ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
782
+
783
+ expect(audit.records()).toContainEqual(expect.objectContaining({
784
+ kind: 'policy-resolved',
785
+ sessionId: String(session.id),
786
+ route: { provider: 'openai', model: 'deepseek-v4-pro' },
787
+ tokenizer: { repository: 'unavailable', revision: 'unavailable' },
788
+ }))
789
+ })
790
+
791
+ it('emits a fresh policy audit when the durable route changes mid-session', async () => {
792
+ const ctx = await runtimeContext()
793
+ const audit = captureAudit(ctx)
794
+ await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
795
+ const session = Session.create(SessionId('public-policy-audit-reroute'))
796
+ appendToolTurn(session, 1, 'first route evidence', false)
797
+ ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
798
+ session.append('request/header', {
799
+ reason: 'change',
800
+ header: canonicalHeader({ config: { provider: 'deepseek', model: 'deepseek-v4-pro' } }),
801
+ })
802
+ appendToolTurn(session, 2, 'second route evidence', false, undefined, 'deepseek', 'deepseek-v4-pro')
803
+ ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 2, freshStep: 1 })
804
+
805
+ const resolved = audit.records().filter(record =>
806
+ record.kind === 'policy-resolved' && record.sessionId === String(session.id))
807
+ expect(resolved).toHaveLength(2)
808
+ expect(resolved[0]).toMatchObject({ route: { model: MODEL } })
809
+ expect(resolved[1]).toMatchObject({ route: { model: 'deepseek-v4-pro' } })
810
+ })
811
+
812
+ it('audits the Auto Compact coordination block on policy resolution', async () => {
813
+ const ctx = await runtimeContext()
814
+ await ctx.plugin(TestSettings).await()
815
+ await ctx.plugin(SelectorHost).await()
816
+ const audit = captureAudit(ctx)
817
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
818
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
819
+ await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
820
+ const session = Session.create(SessionId('public-autocompact-coordination'))
821
+ appendToolTurn(session, 1, 'coordination audit evidence', false)
822
+ session.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
823
+ ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
824
+
825
+ const resolved = audit.records().find(record =>
826
+ record.kind === 'policy-resolved' && record.sessionId === String(session.id))
827
+ expect(resolved).toMatchObject({
828
+ policy: {
829
+ autoCompactTokens: 730_000,
830
+ microDeadlineTokens: 638_750,
831
+ historyTriggerTokens: 456_250,
832
+ },
833
+ contextWindowTokens: 1_000_000,
834
+ coordination: {
835
+ thresholdPercent: 73,
836
+ autoCompactTokens: 730_000,
837
+ microDeadlineTokens: 638_750,
838
+ paramSource: 'auto-compact-linked',
839
+ },
840
+ route: { provider: 'deepseek', model: MODEL },
841
+ tokenizer: {
842
+ repository: 'deepseek-ai/DeepSeek-V4-Pro',
843
+ revision: DEEPSEEK_V4_TOKENIZER_ARTIFACT.revision,
844
+ },
845
+ })
846
+
847
+ // Deployment config overriding every linked History watermark must audit
848
+ // itself as deployment-override, not as linkage-derived.
849
+ const overrideCtx = await runtimeContext()
850
+ await overrideCtx.plugin(TestSettings).await()
851
+ await overrideCtx.plugin(SelectorHost).await()
852
+ const overrideAudit = captureAudit(overrideCtx)
853
+ await overrideCtx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
854
+ await overrideCtx.plugin(ToolResultPruner, {
855
+ profile: 'balanced',
856
+ historyTriggerTokens: 123_456,
857
+ historyKeepRecentTokens: 12_345,
858
+ historyMinReclaimTokens: 1_234,
859
+ }).await()
860
+ const overrideSession = Session.create(SessionId('public-autocompact-deployment-override'))
861
+ appendToolTurn(overrideSession, 1, 'deployment override evidence', false)
862
+ overrideSession.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
863
+ overrideCtx.toolResultPruner.pruneSession(overrideSession, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
864
+ expect(overrideAudit.records()).toContainEqual(expect.objectContaining({
865
+ kind: 'policy-resolved',
866
+ sessionId: String(overrideSession.id),
867
+ coordination: expect.objectContaining({
868
+ microDeadlineTokens: 638_750,
869
+ paramSource: 'deployment-override',
870
+ }),
871
+ }))
872
+
873
+ // A partial override of the linked watermarks reads as mixed.
874
+ const mixedCtx = await runtimeContext()
875
+ await mixedCtx.plugin(TestSettings).await()
876
+ await mixedCtx.plugin(SelectorHost).await()
877
+ const mixedAudit = captureAudit(mixedCtx)
878
+ await mixedCtx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
879
+ await mixedCtx.plugin(ToolResultPruner, {
880
+ profile: 'balanced',
881
+ historyTriggerTokens: 123_456,
882
+ }).await()
883
+ const mixedSession = Session.create(SessionId('public-autocompact-mixed-source'))
884
+ appendToolTurn(mixedSession, 1, 'mixed source evidence', false)
885
+ mixedSession.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
886
+ mixedCtx.toolResultPruner.pruneSession(mixedSession, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
887
+ expect(mixedAudit.records()).toContainEqual(expect.objectContaining({
888
+ kind: 'policy-resolved',
889
+ sessionId: String(mixedSession.id),
890
+ coordination: expect.objectContaining({
891
+ microDeadlineTokens: 638_750,
892
+ paramSource: 'mixed',
893
+ }),
894
+ }))
895
+
896
+ // A route without a verified bundled tokenizer reports the unavailable
897
+ // identity instead of inventing one.
898
+ const unknown = Session.create(SessionId('public-autocompact-unknown-route'))
899
+ appendToolTurn(unknown, 1, 'unknown route evidence', false, undefined, 'deepseek', 'some-other-model')
900
+ ctx.toolResultPruner.pruneSession(unknown, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
901
+ expect(audit.records()).toContainEqual(expect.objectContaining({
902
+ kind: 'policy-resolved',
903
+ sessionId: String(unknown.id),
904
+ route: { provider: 'deepseek', model: 'some-other-model' },
905
+ tokenizer: { repository: 'unavailable', revision: 'unavailable' },
906
+ }))
907
+ })
908
+
909
+ it('freezes the Auto Compact threshold per Session and applies new values only to new Sessions', async () => {
910
+ const ctx = await runtimeContext()
911
+ await ctx.plugin(TestSettings).await()
912
+ await ctx.plugin(SelectorHost).await()
913
+ const audit = captureAudit(ctx)
914
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
915
+ await ctx.plugin(ToolResultPruner, { profile: 'balanced' }).await()
916
+
917
+ const first = Session.create(SessionId('public-autocompact-freeze-first'))
918
+ appendToolTurn(first, 1, 'frozen threshold evidence', false)
919
+ first.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
920
+ ctx.toolResultPruner.pruneSession(first, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
921
+
922
+ await ctx.settings.update(namespace, { autoCompact: { thresholdPercent: 73 } })
923
+
924
+ const second = Session.create(SessionId('public-autocompact-freeze-second'))
925
+ appendToolTurn(second, 1, 'new threshold evidence', false)
926
+ second.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
927
+ ctx.toolResultPruner.pruneSession(second, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
928
+
929
+ // The already-observed session keeps its frozen 80% policy even after
930
+ // another prune; only the new session adopts 73%.
931
+ first.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
932
+ appendToolTurn(first, 2, 'post-change frozen evidence', false)
933
+ first.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 1_000_000 })
934
+ ctx.toolResultPruner.pruneSession(first, { stage: 'fresh', freshTurn: 2, freshStep: 1 })
935
+
936
+ const resolvedFor = (sessionId: string) => audit.records().find(record =>
937
+ record.kind === 'policy-resolved' && record.sessionId === sessionId)
938
+ expect(resolvedFor(String(first.id))).toMatchObject({
939
+ coordination: { thresholdPercent: 80, autoCompactTokens: 800_000, microDeadlineTokens: 700_000 },
940
+ })
941
+ expect(resolvedFor(String(second.id))).toMatchObject({
942
+ coordination: { thresholdPercent: 73, autoCompactTokens: 730_000, microDeadlineTokens: 638_750 },
943
+ })
944
+ expect(audit.records().filter(record =>
945
+ record.kind === 'policy-resolved'
946
+ && record.sessionId === String(first.id)
947
+ && record.coordination?.thresholdPercent === 73)).toHaveLength(0)
948
+ })
949
+
950
+ it('ages History through the full-request last-chance gate even below the profile trigger', async () => {
951
+ const ctx = await runtimeContext()
952
+ const audit = captureAudit(ctx)
953
+ await ctx.plugin(ToolResultPruner, {
954
+ profile: 'balanced',
955
+ freshTriggerTokens: 100_000,
956
+ freshTargetTokens: 90_000,
957
+ aggregateTriggerTokens: 100_000,
958
+ aggregateTargetTokens: 90_000,
959
+ historyTriggerTokens: 100_000,
960
+ historyKeepRecentToolCalls: 0,
961
+ historyKeepRecentTokens: 1,
962
+ historyMinReclaimTokens: 1,
963
+ }).await()
964
+ const session = Session.create(SessionId('public-history-last-chance'))
965
+ session.append('turn/start', { turn: 1 })
966
+ session.append('request/header', {
967
+ reason: 'initial',
968
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
969
+ })
970
+ // Non-tool prose carries the full request past the deadline while the
971
+ // tool results alone stay far below the linked profile trigger.
972
+ session.append('user/message', createUserMessage({
973
+ content: [{ type: 'text', text: 'pressure prose '.repeat(1_000) }],
974
+ source: { kind: 'user' },
975
+ }), { surfaceOp: 'append' })
976
+ session.append('step/start', { turn: 1, step: 1 })
977
+ session.append('assistant/message', {
978
+ turn: 1,
979
+ step: 1,
980
+ message: createMessage({
981
+ role: 'assistant',
982
+ content: [{ type: 'tool-call', id: CallId('last-chance-old'), name: 'bash', arguments: '{}' }],
983
+ source: { kind: 'model', provider: 'deepseek', model: MODEL },
984
+ }),
985
+ }, { surfaceOp: 'append' })
986
+ session.append('tool/call', { turn: 1, step: 1, callId: CallId('last-chance-old'), name: 'bash', arguments: '{}' })
987
+ session.append('tool/result', {
988
+ turn: 1,
989
+ step: 1,
990
+ message: createToolResultMessage({
991
+ callId: CallId('last-chance-old'),
992
+ content: [{ type: 'text', text: 'last-chance aging evidence '.repeat(700) }],
993
+ isError: false,
994
+ }),
995
+ }, { surfaceOp: 'append' })
996
+ session.append('step/end', { turn: 1, step: 1 })
997
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
998
+ appendToolTurn(session, 2, 'newest protected result', true)
999
+ // 80% default on a 10,000-token window: A = 8000, linked H = 5000, D = 7000.
1000
+ session.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 10_000 })
1001
+ session.append('turn/start', { turn: 3 })
1002
+
1003
+ const before = measureForCompaction(ctx, session)
1004
+ const toolResultTokens = before.measuredNodes
1005
+ .filter(node => session.events[node.seq]?.type === 'tool/result')
1006
+ .reduce((sum, node) => sum + (node.count.kind === 'exact-tokenizer' ? node.count.tokens : 0), 0)
1007
+ // Deadline D = 7000 is reached by the complete request, not by the tool
1008
+ // results, which stay far below every trigger in force.
1009
+ expect(before.totalTokens).toBeGreaterThanOrEqual(7_000)
1010
+ expect(toolResultTokens).toBeLessThan(7_000)
1011
+
1012
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1013
+
1014
+ const aged = rewrites(audit.records()).filter(record =>
1015
+ record.sessionId === String(session.id) && record.component === 'history')
1016
+ expect(aged.length).toBeGreaterThanOrEqual(1)
1017
+ expect(result.pruned.length).toBeGreaterThanOrEqual(1)
1018
+ })
1019
+
1020
+ it.each(['balanced', 'adaptive'] as const)(
1021
+ 'skips a linked History batch that cannot reach its reclaim target in %s mode',
1022
+ async (profile) => {
1023
+ const ctx = await runtimeContext()
1024
+ const audit = captureAudit(ctx)
1025
+ await ctx.plugin(ToolResultPruner, {
1026
+ profile,
1027
+ freshTriggerTokens: 100_000,
1028
+ freshTargetTokens: 90_000,
1029
+ aggregateTriggerTokens: 100_000,
1030
+ aggregateTargetTokens: 90_000,
1031
+ historyTriggerTokens: 100,
1032
+ historyKeepRecentToolCalls: 0,
1033
+ historyKeepRecentTokens: 1,
1034
+ historyMinReclaimTokens: 1,
1035
+ }).await()
1036
+
1037
+ // Below the deadline: the excess above the trigger sits mostly in the
1038
+ // protected newest result, so the only reclaimable batch is far too small.
1039
+ const below = Session.create(SessionId(`public-history-insufficient-reclaim-${profile}`))
1040
+ appendToolTurn(below, 1, 'unreachable reclaim evidence '.repeat(400), true)
1041
+ appendToolTurn(below, 2, 'newest protected bulk '.repeat(600), true)
1042
+ below.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 10_000 })
1043
+ below.append('turn/start', { turn: 3 })
1044
+ const belowResult = ctx.toolResultPruner.pruneSession(below, { stage: 'pressure' })
1045
+ expect(belowResult.pruned).toHaveLength(0)
1046
+ expect(rewrites(audit.records()).some(record =>
1047
+ record.sessionId === String(below.id) && record.component === 'history')).toBe(false)
1048
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1049
+ kind: 'component-evaluation',
1050
+ sessionId: String(below.id),
1051
+ component: 'history',
1052
+ status: 'skipped',
1053
+ reason: 'insufficient-reclaim',
1054
+ reclaimTokens: expect.any(Number),
1055
+ requiredTokens: expect.any(Number),
1056
+ }))
1057
+
1058
+ // Past the deadline with tool tokens below the trigger: the last-chance
1059
+ // gate engages planning, and the skip names the deadline target.
1060
+ const past = Session.create(SessionId(`public-history-deadline-unreachable-${profile}`))
1061
+ past.append('turn/start', { turn: 1 })
1062
+ past.append('request/header', {
1063
+ reason: 'initial',
1064
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
1065
+ })
1066
+ past.append('user/message', createUserMessage({
1067
+ content: [{ type: 'text', text: 'deadline pressure prose '.repeat(2_000) }],
1068
+ source: { kind: 'user' },
1069
+ }), { surfaceOp: 'append' })
1070
+ appendToolTurn(past, 1, 'deadline unreachable evidence '.repeat(400), true)
1071
+ appendToolTurn(past, 2, 'newest protected bulk '.repeat(600), true)
1072
+ past.append('request/context', { provider: 'deepseek', model: MODEL, contextWindow: 10_000 })
1073
+ past.append('turn/start', { turn: 2 })
1074
+ const pastResult = ctx.toolResultPruner.pruneSession(past, { stage: 'pressure' })
1075
+ expect(pastResult.pruned).toHaveLength(0)
1076
+ expect(rewrites(audit.records()).some(record =>
1077
+ record.sessionId === String(past.id) && record.component === 'history')).toBe(false)
1078
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1079
+ kind: 'component-evaluation',
1080
+ sessionId: String(past.id),
1081
+ component: 'history',
1082
+ status: 'skipped',
1083
+ reason: 'cannot-reach-deadline-target',
1084
+ triggerTokens: 7_000,
1085
+ }))
1086
+ },
1087
+ )
1088
+
1089
+ it.each(['balanced', 'adaptive'] as const)(
1090
+ 'audits below-profile-trigger in %s mode when the tool-result total sits under the History trigger',
1091
+ async (profile) => {
1092
+ const ctx = await runtimeContext()
1093
+ const audit = captureAudit(ctx)
1094
+ await ctx.plugin(ToolResultPruner, {
1095
+ profile,
1096
+ freshTriggerTokens: 100_000,
1097
+ freshTargetTokens: 90_000,
1098
+ aggregateTriggerTokens: 100_000,
1099
+ aggregateTargetTokens: 90_000,
1100
+ historyTriggerTokens: 100_000,
1101
+ historyKeepRecentToolCalls: 0,
1102
+ historyKeepRecentTokens: 1,
1103
+ historyMinReclaimTokens: 1,
1104
+ }).await()
1105
+ const session = Session.create(SessionId(`public-history-below-profile-trigger-${profile}`))
1106
+ appendToolTurn(session, 1, 'small routine evidence', true)
1107
+ session.append('turn/start', { turn: 2 })
1108
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1109
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1110
+ kind: 'component-evaluation',
1111
+ sessionId: String(session.id),
1112
+ component: 'history',
1113
+ status: 'skipped',
1114
+ reason: 'below-profile-trigger',
1115
+ triggerTokens: 100_000,
1116
+ }))
1117
+ },
1118
+ )
1119
+
1120
+ it.each(['balanced', 'adaptive'] as const)(
1121
+ 'audits protected-working-set in %s mode when every safe candidate is inside the protected tail',
1122
+ async (profile) => {
1123
+ const ctx = await runtimeContext()
1124
+ const audit = captureAudit(ctx)
1125
+ await ctx.plugin(ToolResultPruner, {
1126
+ profile,
1127
+ freshTriggerTokens: 100_000,
1128
+ freshTargetTokens: 90_000,
1129
+ aggregateTriggerTokens: 100_000,
1130
+ aggregateTargetTokens: 90_000,
1131
+ historyTriggerTokens: 100,
1132
+ historyKeepRecentToolCalls: 10,
1133
+ historyKeepRecentTokens: 1_000_000,
1134
+ historyMinReclaimTokens: 1,
1135
+ }).await()
1136
+ const session = Session.create(SessionId(`public-history-protected-working-set-${profile}`))
1137
+ appendToolTurn(session, 1, 'protected working set evidence '.repeat(300), true)
1138
+ appendToolTurn(session, 2, 'newest protected result '.repeat(300), true)
1139
+ session.append('turn/start', { turn: 3 })
1140
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1141
+ expect(result.pruned).toHaveLength(0)
1142
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1143
+ kind: 'component-evaluation',
1144
+ sessionId: String(session.id),
1145
+ component: 'history',
1146
+ status: 'skipped',
1147
+ reason: 'protected-working-set',
1148
+ }))
1149
+ },
1150
+ )
1151
+
1152
+ it.each(['balanced', 'adaptive'] as const)(
1153
+ 'audits no-safe-candidates in %s mode when the only tool results are recovery-tool output',
1154
+ async (profile) => {
1155
+ const ctx = await runtimeContext()
1156
+ const audit = captureAudit(ctx)
1157
+ await ctx.plugin(ToolResultPruner, {
1158
+ profile,
1159
+ freshTriggerTokens: 100_000,
1160
+ freshTargetTokens: 90_000,
1161
+ aggregateTriggerTokens: 100_000,
1162
+ aggregateTargetTokens: 90_000,
1163
+ historyTriggerTokens: 100,
1164
+ historyKeepRecentToolCalls: 0,
1165
+ historyKeepRecentTokens: 1,
1166
+ historyMinReclaimTokens: 1,
1167
+ }).await()
1168
+ const session = Session.create(SessionId(`public-history-no-safe-candidates-${profile}`))
1169
+ // One ordinary turn (protected by nothing) plus one recovery-tool output:
1170
+ // filtering only unsafe candidates leaves the ordinary result, so this
1171
+ // first pass must NOT read as no-safe-candidates.
1172
+ appendToolTurn(session, 1, 'ordinary reclaimable evidence '.repeat(300), true)
1173
+ const turn = 2
1174
+ const callId = CallId('recovery-call')
1175
+ session.append('turn/start', { turn })
1176
+ session.append('step/start', { turn, step: 1 })
1177
+ session.append('assistant/message', {
1178
+ turn,
1179
+ step: 1,
1180
+ message: createMessage({
1181
+ role: 'assistant',
1182
+ content: [{ type: 'tool-call', id: callId, name: 'context_compression_retrieve', arguments: '{}' }],
1183
+ source: { kind: 'model', provider: 'deepseek', model: MODEL },
1184
+ }),
1185
+ }, { surfaceOp: 'append' })
1186
+ session.append('tool/call', { turn, step: 1, callId, name: 'context_compression_retrieve', arguments: '{}' })
1187
+ session.append('tool/result', {
1188
+ turn,
1189
+ step: 1,
1190
+ message: createToolResultMessage({
1191
+ callId,
1192
+ content: [{ type: 'text', text: 'recovered original group '.repeat(300) }],
1193
+ isError: false,
1194
+ }),
1195
+ }, { surfaceOp: 'append' })
1196
+ session.append('step/end', { turn, step: 1 })
1197
+ session.append('turn/end', { turn, reason: { kind: 'completed' } })
1198
+ session.append('turn/start', { turn: 3 })
1199
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1200
+ // The ordinary result remains a safe candidate. Balanced commits it;
1201
+ // Adaptive may reject its real plan on cost, but neither profile may
1202
+ // misreport this mixed set as no-safe-candidates.
1203
+ expect(audit.records().some(record => record.kind === 'component-evaluation'
1204
+ && record.sessionId === String(session.id)
1205
+ && record.component === 'history'
1206
+ && record.reason === 'no-safe-candidates')).toBe(false)
1207
+ if (profile === 'balanced') {
1208
+ expect(rewrites(audit.records()).some(record =>
1209
+ record.sessionId === String(session.id) && record.component === 'history')).toBe(true)
1210
+ } else {
1211
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1212
+ kind: 'component-evaluation',
1213
+ sessionId: String(session.id),
1214
+ component: 'history',
1215
+ reason: 'adaptive-cost-rejected',
1216
+ }))
1217
+ }
1218
+
1219
+ const onlyUnsafe = Session.create(SessionId(`public-history-no-safe-candidates-only-${profile}`))
1220
+ onlyUnsafe.append('turn/start', { turn: 1 })
1221
+ onlyUnsafe.append('request/header', {
1222
+ reason: 'initial',
1223
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
1224
+ })
1225
+ onlyUnsafe.append('step/start', { turn: 1, step: 1 })
1226
+ onlyUnsafe.append('assistant/message', {
1227
+ turn: 1,
1228
+ step: 1,
1229
+ message: createMessage({
1230
+ role: 'assistant',
1231
+ content: [{ type: 'tool-call', id: callId, name: 'context_compression_retrieve', arguments: '{}' }],
1232
+ source: { kind: 'model', provider: 'deepseek', model: MODEL },
1233
+ }),
1234
+ }, { surfaceOp: 'append' })
1235
+ onlyUnsafe.append('tool/call', { turn: 1, step: 1, callId, name: 'context_compression_retrieve', arguments: '{}' })
1236
+ onlyUnsafe.append('tool/result', {
1237
+ turn: 1,
1238
+ step: 1,
1239
+ message: createToolResultMessage({
1240
+ callId,
1241
+ content: [{ type: 'text', text: 'only recovery output lives here '.repeat(300) }],
1242
+ isError: false,
1243
+ }),
1244
+ }, { surfaceOp: 'append' })
1245
+ onlyUnsafe.append('step/end', { turn: 1, step: 1 })
1246
+ onlyUnsafe.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
1247
+ onlyUnsafe.append('turn/start', { turn: 2 })
1248
+ ctx.toolResultPruner.pruneSession(onlyUnsafe, { stage: 'pressure' })
1249
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1250
+ kind: 'component-evaluation',
1251
+ sessionId: String(onlyUnsafe.id),
1252
+ component: 'history',
1253
+ status: 'skipped',
1254
+ reason: 'no-safe-candidates',
1255
+ }))
1256
+ },
1257
+ )
1258
+
1259
+ it('audits adaptive-cost-rejected when the adaptive estimate refuses the batch', async () => {
1260
+ const ctx = await runtimeContext()
1261
+ const audit = captureAudit(ctx)
1262
+ await ctx.plugin(ToolResultPruner, {
1263
+ profile: 'adaptive',
1264
+ freshTriggerTokens: 100_000,
1265
+ freshTargetTokens: 90_000,
1266
+ aggregateTriggerTokens: 100_000,
1267
+ aggregateTargetTokens: 90_000,
1268
+ historyTriggerTokens: 100,
1269
+ historyKeepRecentToolCalls: 0,
1270
+ historyKeepRecentTokens: 1,
1271
+ historyMinReclaimTokens: 1,
1272
+ }).await()
1273
+ const session = Session.create(SessionId('public-history-adaptive-cost-rejected'))
1274
+ appendToolTurn(session, 1, 'adaptive rejected evidence '.repeat(300), true)
1275
+ appendToolTurn(session, 2, 'newest adaptive working-set evidence', true)
1276
+ session.append('turn/start', { turn: 3 })
1277
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1278
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1279
+ kind: 'component-evaluation',
1280
+ sessionId: String(session.id),
1281
+ component: 'history',
1282
+ status: 'skipped',
1283
+ reason: 'adaptive-cost-rejected',
1284
+ historyMode: 'adaptive',
1285
+ }))
1286
+ })
1287
+
1288
+ it('preserves below-profile-trigger when Adaptive planning never forms a batch', async () => {
1289
+ const ctx = await runtimeContext()
1290
+ const audit = captureAudit(ctx)
1291
+ await ctx.plugin(ToolResultPruner, {
1292
+ profile: 'adaptive',
1293
+ freshTriggerTokens: 100_000,
1294
+ freshTargetTokens: 90_000,
1295
+ aggregateTriggerTokens: 100_000,
1296
+ aggregateTargetTokens: 90_000,
1297
+ historyTriggerTokens: 100_000,
1298
+ historyKeepRecentToolCalls: 0,
1299
+ historyKeepRecentTokens: 1,
1300
+ historyMinReclaimTokens: 1,
1301
+ }).await()
1302
+ const session = Session.create(SessionId('public-history-adaptive-below-trigger'))
1303
+ appendToolTurn(session, 1, 'small adaptive evidence', true)
1304
+ session.append('turn/start', { turn: 2 })
1305
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1306
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1307
+ kind: 'component-evaluation',
1308
+ sessionId: String(session.id),
1309
+ component: 'history',
1310
+ status: 'skipped',
1311
+ reason: 'below-profile-trigger',
1312
+ historyMode: 'adaptive',
1313
+ triggerTokens: 100_000,
1314
+ }))
1315
+ })
1316
+
1317
+ it('preserves exact-tokenizer-unavailable before Adaptive cost authority', async () => {
1318
+ const ctx = await runtimeContext()
1319
+ const audit = captureAudit(ctx)
1320
+ await ctx.plugin(ToolResultPruner, {
1321
+ profile: 'adaptive',
1322
+ freshTriggerTokens: 100_000,
1323
+ freshTargetTokens: 90_000,
1324
+ aggregateTriggerTokens: 100_000,
1325
+ aggregateTargetTokens: 90_000,
1326
+ historyTriggerTokens: 100,
1327
+ historyKeepRecentToolCalls: 0,
1328
+ historyKeepRecentTokens: 1,
1329
+ historyMinReclaimTokens: 1,
1330
+ }).await()
1331
+ const session = Session.create(SessionId('public-history-adaptive-exact-unavailable'))
1332
+ appendToolTurn(
1333
+ session,
1334
+ 1,
1335
+ 'unknown adaptive model evidence '.repeat(300),
1336
+ true,
1337
+ undefined,
1338
+ 'deepseek',
1339
+ 'unsupported-public-model',
1340
+ )
1341
+ session.append('turn/start', { turn: 2 })
1342
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1343
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1344
+ kind: 'component-evaluation',
1345
+ sessionId: String(session.id),
1346
+ component: 'history',
1347
+ status: 'skipped',
1348
+ reason: 'exact-tokenizer-unavailable',
1349
+ historyMode: 'adaptive',
1350
+ measurementKind: 'unavailable',
1351
+ }))
1352
+ })
1353
+
1354
+ it('audits exact-tokenizer-unavailable for History when counts are not exact', async () => {
1355
+ const ctx = await runtimeContext()
1356
+ const audit = captureAudit(ctx)
1357
+ await ctx.plugin(ToolResultPruner, {
1358
+ profile: 'balanced',
1359
+ freshTriggerTokens: 100_000,
1360
+ freshTargetTokens: 90_000,
1361
+ aggregateTriggerTokens: 100_000,
1362
+ aggregateTargetTokens: 90_000,
1363
+ historyTriggerTokens: 100,
1364
+ historyKeepRecentToolCalls: 0,
1365
+ historyKeepRecentTokens: 1,
1366
+ historyMinReclaimTokens: 1,
1367
+ }).await()
1368
+ const session = Session.create(SessionId('public-history-exact-unavailable'))
1369
+ appendToolTurn(session, 1, 'unknown model history evidence '.repeat(300), true, undefined, 'deepseek', 'unsupported-public-model')
1370
+ session.append('turn/start', { turn: 2 })
1371
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1372
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1373
+ kind: 'component-evaluation',
1374
+ sessionId: String(session.id),
1375
+ component: 'history',
1376
+ status: 'skipped',
1377
+ reason: 'exact-tokenizer-unavailable',
1378
+ measurementKind: 'unavailable',
1379
+ }))
1380
+ })
1381
+
1382
+ it('audits recovery-tool-unavailable when a committed batch cannot land without the recovery tool', async () => {
1383
+ const ctx = new Context()
1384
+ activeContexts.push(ctx)
1385
+ await ctx.plugin(SessionStore)
1386
+ await ctx.plugin(SystemPrompt)
1387
+ await ctx.plugin(TokenMeter)
1388
+ const audit = captureAudit(ctx)
1389
+ await ctx.plugin(ToolResultPruner, {
1390
+ profile: 'balanced',
1391
+ freshTriggerTokens: 100_000,
1392
+ freshTargetTokens: 90_000,
1393
+ aggregateTriggerTokens: 100_000,
1394
+ aggregateTargetTokens: 90_000,
1395
+ historyTriggerTokens: 100,
1396
+ historyKeepRecentToolCalls: 0,
1397
+ historyKeepRecentTokens: 1,
1398
+ historyMinReclaimTokens: 1,
1399
+ }).await()
1400
+ const session = Session.create(SessionId('public-history-recovery-tool-unavailable'))
1401
+ // The newest result stays inside the protected tail; the older one plans a
1402
+ // committed batch that cannot land without the recovery tool.
1403
+ appendToolTurn(session, 1, 'committed batch without recovery tool '.repeat(300), true)
1404
+ appendToolTurn(session, 2, 'newest protected result', true)
1405
+ session.append('turn/start', { turn: 3 })
1406
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1407
+ expect(result.pruned).toHaveLength(0)
1408
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1409
+ kind: 'component-evaluation',
1410
+ sessionId: String(session.id),
1411
+ component: 'history',
1412
+ status: 'skipped',
1413
+ reason: 'recovery-tool-unavailable',
1414
+ }))
1415
+ })
1416
+
1417
+ it('still commits unlinked Custom History batches at the minimum reclaim', async () => {
1418
+ const ctx = await runtimeContext()
1419
+ await ctx.plugin(TestSettings).await()
1420
+ await ctx.plugin(SelectorHost).await()
1421
+ const audit = captureAudit(ctx)
1422
+ // A protected token tail larger than the trigger leaves the linked-style
1423
+ // whole-excess demand unreachable; Custom stays manual and must still
1424
+ // commit once a batch passes its explicit minimum reclaim.
1425
+ const custom = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
1426
+ if (custom.version !== 3) throw new Error('unlinked Custom fixture requires policy v3')
1427
+ custom.fresh = { enabled: true, trigger: 1_000_000, target: 900_000 }
1428
+ custom.aggregate = { enabled: true, trigger: 1_000_000, target: 900_000 }
1429
+ custom.history = {
1430
+ enabled: true,
1431
+ trigger: 20_000,
1432
+ keepRecentToolCalls: 2,
1433
+ keepRecentTokens: 30_000,
1434
+ minReclaim: 1_000,
1435
+ }
1436
+ custom.prefixPolicy = 'pressure-break'
1437
+ custom.tailTrim = { enabled: false, trigger: 700_000 }
1438
+ await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1439
+ profile: 'custom',
1440
+ custom,
1441
+ })
1442
+ await ctx.plugin(ToolResultPruner, { profile: 'custom' }).await()
1443
+
1444
+ const session = Session.create(SessionId('public-custom-min-reclaim-commit'))
1445
+ // Five results of ~14k tokens: the 30k token tail plus 2 newest calls
1446
+ // protect the newest three, leaving two old results reclaimable.
1447
+ appendToolTurn(session, 1, 'custom old reclaimable evidence '.repeat(3_000), true)
1448
+ appendToolTurn(session, 2, 'custom old reclaimable evidence '.repeat(3_000), true)
1449
+ appendToolTurn(session, 3, 'custom newer reclaimable evidence '.repeat(3_000), true)
1450
+ appendToolTurn(session, 4, 'custom newer reclaimable evidence '.repeat(3_000), true)
1451
+ appendToolTurn(session, 5, 'custom newest protected evidence '.repeat(3_000), true)
1452
+ session.append('turn/start', { turn: 6 })
1453
+
1454
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1455
+
1456
+ expect(result.pruned.length).toBeGreaterThanOrEqual(1)
1457
+ expect(rewrites(audit.records()).some(record =>
1458
+ record.sessionId === String(session.id) && record.component === 'history')).toBe(true)
1459
+ })
1460
+
1461
+ it('distinguishes inactive and active History capacity-pressure from durable route capacity', async () => {
1462
+ const ctx = await runtimeContext()
1463
+ const audit = captureAudit(ctx)
1464
+ await ctx.plugin(ToolResultPruner, {
1465
+ profile: 'cache-strict',
1466
+ freshTriggerTokens: 100_000,
1467
+ freshTargetTokens: 90_000,
1468
+ aggregateTriggerTokens: 100_000,
1469
+ aggregateTargetTokens: 90_000,
1470
+ historyTriggerTokens: 400,
1471
+ historyKeepRecentToolCalls: 0,
1472
+ historyKeepRecentTokens: 1,
1473
+ historyMinReclaimTokens: 1,
1474
+ }).await()
1475
+
1476
+ const belowCapacityGate = Session.create(SessionId('public-history-capacity-inactive'))
1477
+ appendToolTurn(belowCapacityGate, 1, 'capacity inactive evidence '.repeat(600), true)
1478
+ appendToolTurn(belowCapacityGate, 2, 'newest protected result', true)
1479
+ belowCapacityGate.append('request/context', {
1480
+ provider: 'deepseek',
1481
+ model: MODEL,
1482
+ contextWindow: 1_000_000,
1483
+ })
1484
+ belowCapacityGate.append('turn/start', { turn: 3 })
1485
+ ctx.toolResultPruner.pruneSession(belowCapacityGate, { stage: 'pressure' })
1486
+
1487
+ expect(rewrites(audit.records()).some(record =>
1488
+ record.sessionId === String(belowCapacityGate.id) && record.component === 'history')).toBe(false)
1489
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1490
+ kind: 'component-evaluation',
1491
+ sessionId: String(belowCapacityGate.id),
1492
+ component: 'history',
1493
+ status: 'skipped',
1494
+ reason: 'below-micro-deadline',
1495
+ historyMode: 'capacity-pressure',
1496
+ }))
1497
+
1498
+ const aboveCapacityGate = Session.create(SessionId('public-history-capacity-active'))
1499
+ appendToolTurn(aboveCapacityGate, 1, 'capacity active evidence '.repeat(600), true)
1500
+ appendToolTurn(aboveCapacityGate, 2, 'newest protected result', true)
1501
+ const capacityWindow = Math.floor(measureForCompaction(ctx, aboveCapacityGate).totalTokens / 0.75)
1502
+ aboveCapacityGate.append('request/context', {
1503
+ provider: 'deepseek',
1504
+ model: MODEL,
1505
+ contextWindow: capacityWindow,
1506
+ })
1507
+ aboveCapacityGate.append('turn/start', { turn: 3 })
1508
+ ctx.toolResultPruner.pruneSession(aboveCapacityGate, { stage: 'pressure' })
1509
+
1510
+ expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
1511
+ sessionId: String(aboveCapacityGate.id),
1512
+ component: 'history',
1513
+ stage: 'pressure',
1514
+ historyMode: 'capacity-pressure',
1515
+ }))
1516
+ })
1517
+
1518
+ it('runs Cache Strict capacity-pressure History through the real agent pre-step boundary', async () => {
1519
+ const ctx = new Context()
1520
+ activeContexts.push(ctx)
1521
+ await mountAgentLoopTestDependencies(ctx)
1522
+ await ctx.plugin(AgentLoop, { agents: [] })
1523
+ await ctx.plugin(TokenMeter)
1524
+ const audit = captureAudit(ctx)
1525
+ ctx.llm.registerAdapter(
1526
+ ['deepseek'],
1527
+ new NativeSummaryAdapter(['Cache Strict pressure pass complete'], 100),
1528
+ )
1529
+ await ctx.plugin(ToolResultPruner, {
1530
+ profile: 'cache-strict',
1531
+ freshTriggerTokens: 100_000,
1532
+ freshTargetTokens: 90_000,
1533
+ aggregateTriggerTokens: 100_000,
1534
+ aggregateTargetTokens: 90_000,
1535
+ historyTriggerTokens: 400,
1536
+ historyKeepRecentToolCalls: 0,
1537
+ historyKeepRecentTokens: 1,
1538
+ historyMinReclaimTokens: 1,
1539
+ }).await()
1540
+
1541
+ const agent = ctx.agentLoop.create(SessionId('public-history-capacity-request-boundary'), {
1542
+ provider: 'deepseek',
1543
+ model: MODEL,
1544
+ })
1545
+ const { session } = agent
1546
+ appendToolTurn(session, 1, 'old capacity-pressure evidence '.repeat(600), true)
1547
+ appendToolTurn(session, 2, 'newest protected result', true)
1548
+ const boundaryTotal = measureForCompaction(ctx, session).totalTokens
1549
+ session.append('request/context', {
1550
+ provider: 'deepseek',
1551
+ model: MODEL,
1552
+ // Just under the frozen 80% deadline D = 0.7 * window, so the gate is
1553
+ // active while the reclaim target stays reachable for one old result.
1554
+ contextWindow: Math.floor(boundaryTotal / 0.72),
1555
+ })
1556
+
1557
+ agent.followup(createUserMessage({
1558
+ content: [{ type: 'text', text: 'run Cache Strict pressure pass' }],
1559
+ source: { kind: 'user' },
1560
+ }))
1561
+ await agent.whenIdle()
1562
+
1563
+ expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
1564
+ sessionId: String(session.id),
1565
+ component: 'history',
1566
+ stage: 'pressure',
1567
+ historyMode: 'capacity-pressure',
1568
+ }))
1569
+ })
1570
+
1571
+ it('audits enabled-but-below-trigger without claiming a rewrite', async () => {
1572
+ const ctx = await runtimeContext()
1573
+ const audit = captureAudit(ctx)
1574
+ await ctx.plugin(ToolResultPruner, {
1575
+ profile: 'balanced',
1576
+ freshTriggerTokens: 100_000,
1577
+ freshTargetTokens: 90_000,
1578
+ aggregateTriggerTokens: 100_000,
1579
+ aggregateTargetTokens: 90_000,
1580
+ historyTriggerTokens: 100_000,
1581
+ }).await()
1582
+ const session = Session.create(SessionId('public-below-trigger-audit'))
1583
+ appendToolTurn(session, 1, 'small evidence', false)
1584
+
1585
+ ctx.toolResultPruner.pruneSession(session, {
1586
+ stage: 'fresh',
1587
+ freshTurn: 1,
1588
+ freshStep: 1,
1589
+ })
1590
+
1591
+ expect(rewrites(audit.records())).toHaveLength(0)
1592
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1593
+ kind: 'component-evaluation',
1594
+ component: 'fresh',
1595
+ status: 'skipped',
1596
+ reason: 'at-or-below-trigger',
1597
+ currentTokens: expect.any(Number),
1598
+ triggerTokens: 100_000,
1599
+ }))
1600
+ })
1601
+
1602
+ it('freezes the complete settings and deployment snapshot on first Session observation', async () => {
1603
+ const ctx = new Context()
1604
+ activeContexts.push(ctx)
1605
+ await ctx.plugin(TestSettings).await()
1606
+ await ctx.plugin(SelectorHost).await()
1607
+ await ctx.plugin(SessionStore)
1608
+ await ctx.plugin(SystemPrompt)
1609
+ await ctx.plugin(ToolRuntime)
1610
+ await ctx.plugin(TokenMeter)
1611
+ const audit = captureAudit(ctx)
1612
+
1613
+ const firstCustom = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
1614
+ firstCustom.fresh = { enabled: true, trigger: 512, target: 256 }
1615
+ firstCustom.aggregate.enabled = false
1616
+ firstCustom.history.enabled = false
1617
+ if (firstCustom.version === 3) firstCustom.tailTrim.enabled = false
1618
+ const namespace = settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE)
1619
+ await ctx.settings.update(namespace, { profile: 'custom', custom: firstCustom })
1620
+ await ctx.plugin(ToolResultPruner, {
1621
+ profile: 'off',
1622
+ headChars: 6,
1623
+ tailChars: 4,
1624
+ }).await()
1625
+
1626
+ const sessionA = Session.create(SessionId('public-policy-freeze-a'))
1627
+ appendToolTurn(sessionA, 1, 'first frozen policy result '.repeat(800), false)
1628
+ expect(ctx.toolResultPruner.pruneSession(sessionA, {
1629
+ stage: 'fresh', freshTurn: 1, freshStep: 1,
1630
+ }).pruned).toHaveLength(1)
1631
+
1632
+ const secondCustom = structuredClone(firstCustom)
1633
+ secondCustom.fresh.enabled = false
1634
+ secondCustom.history.trigger += 123
1635
+ await ctx.settings.update(namespace, { custom: secondCustom })
1636
+ sessionA.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
1637
+ appendToolTurn(sessionA, 2, 'second frozen policy result '.repeat(800), false)
1638
+ expect(ctx.toolResultPruner.pruneSession(sessionA, {
1639
+ stage: 'fresh', freshTurn: 2, freshStep: 1,
1640
+ }).pruned).toHaveLength(1)
1641
+
1642
+ const sessionB = Session.create(SessionId('public-policy-freeze-b'))
1643
+ const keptB = appendToolTurn(sessionB, 1, 'new Session disabled Fresh '.repeat(800), false)
1644
+ expect(ctx.toolResultPruner.pruneSession(sessionB, {
1645
+ stage: 'fresh', freshTurn: 1, freshStep: 1,
1646
+ })).toEqual({ pruned: [], charsRemoved: 0, tokensRemoved: 0 })
1647
+ expect(sessionB.surface.nodes).toContain(keptB.resultSeq)
1648
+
1649
+ const frozen = audit.records().filter(record => record.kind === 'policy-frozen')
1650
+ expect(frozen).toHaveLength(2)
1651
+ expect(frozen.find(record => record.sessionId === String(sessionA.id))).toMatchObject({
1652
+ settingsSource: 'host-settings',
1653
+ settings: { profile: 'custom', custom: firstCustom },
1654
+ deploymentConfig: { profile: 'off', headChars: 6, tailChars: 4 },
1655
+ })
1656
+ expect(frozen.find(record => record.sessionId === String(sessionB.id))).toMatchObject({
1657
+ settingsSource: 'host-settings',
1658
+ settings: { profile: 'custom', custom: secondCustom },
1659
+ deploymentConfig: { profile: 'off', headChars: 6, tailChars: 4 },
1660
+ })
1661
+ })
1662
+
1663
+ it('publishes TailTrim through standard prune + user replacement and validates append roots', async () => {
1664
+ const ctx = new Context()
1665
+ activeContexts.push(ctx)
1666
+ await ctx.plugin(TestSettings).await()
1667
+ await ctx.plugin(SelectorHost).await()
1668
+ await ctx.plugin(SessionStore)
1669
+ await ctx.plugin(SystemPrompt)
1670
+ await ctx.plugin(ToolRuntime)
1671
+ await ctx.plugin(TokenMeter)
1672
+ const audit = captureAudit(ctx)
1673
+
1674
+ const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
1675
+ if (policy.version !== 3) throw new Error('TailTrim public protocol test requires policy v3')
1676
+ policy.fresh.enabled = false
1677
+ policy.aggregate.enabled = false
1678
+ policy.history.enabled = false
1679
+ // Isolate TailTrim: keep no History working set protected.
1680
+ policy.history.keepRecentToolCalls = 0
1681
+ policy.history.keepRecentTokens = 0
1682
+ policy.history.minReclaim = 1
1683
+ policy.tailTrim = { enabled: true, trigger: 8 }
1684
+ await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1685
+ profile: 'custom',
1686
+ custom: policy,
1687
+ })
1688
+ await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
1689
+ const pruner = ctx.toolResultPruner
1690
+ const session = Session.create(SessionId('public-tailtrim'))
1691
+ appendToolTurn(session, 1, 'first completed group'.repeat(40), true)
1692
+ const candidate = appendToolTurn(session, 2, 'old candidate result'.repeat(400), true)
1693
+ session.append('turn/start', { turn: 3 })
1694
+ expect(ctx.tools.get('context_compression_retrieve')).toBeDefined()
1695
+ const before = measureForCompaction(ctx, session)
1696
+ expect(before.currentSurface.kind).toBe('exact-tokenizer')
1697
+
1698
+ pruner.pruneSession(session, { stage: 'pressure' })
1699
+
1700
+ const manifest = session.events.findLast(event => event.type === 'compaction/prune')
1701
+ expect(manifest?.type).toBe('compaction/prune')
1702
+ if (manifest?.type !== 'compaction/prune') throw new Error('TailTrim did not publish a standard prune')
1703
+ const publication = validatePublishedTailTrim(session, manifest.seq)
1704
+ expect(publication).not.toBeNull()
1705
+ expect(publication?.manifest.data.shadowedSeqs).toEqual([
1706
+ candidate.assistantSeq,
1707
+ candidate.resultSeq,
1708
+ ])
1709
+ expect(publication?.replacement.type).toBe('user/message')
1710
+ expect(publication?.ref).toBe(`session://${String(session.id)}/tailtrim/${String(manifest.seq)}`)
1711
+ expect(ctx.tools.get('context_compression_retrieve')).toBeDefined()
1712
+ expect(rewrites(audit.records())).toContainEqual(expect.objectContaining({
1713
+ component: 'tail-trim',
1714
+ stage: 'pressure',
1715
+ reducer: 'pair-preserving-tail-trim',
1716
+ manifestSeq: manifest.seq,
1717
+ tokensBefore: expect.any(Number),
1718
+ tokensAfter: expect.any(Number),
1719
+ }))
1720
+
1721
+ const recovered = await ctx.tools.execute({
1722
+ name: 'context_compression_retrieve',
1723
+ arguments: { ref: publication?.ref, max_lines: 20 },
1724
+ callId: CallId('tailtrim-retrieve-live'),
1725
+ signal: new AbortController().signal,
1726
+ agent: stubAgent(ctx, session),
1727
+ })
1728
+ expect(recovered.isError).toBe(false)
1729
+ const recoveredText = recovered.content
1730
+ .map(block => block.type === 'text' ? block.text : '')
1731
+ .join('\n')
1732
+ expect(recoveredText).toContain('kind: tailtrim-group')
1733
+ expect(recoveredText).toContain('old candidate result')
1734
+
1735
+ const persistedEvents = JSON.parse(JSON.stringify(session.events))
1736
+ const replay = Session.create(session.id, persistedEvents)
1737
+ const replayPublication = validatePublishedTailTrim(replay, manifest.seq)
1738
+ expect(replayPublication?.ref).toBe(publication?.ref)
1739
+ expect(replay.deriveMessages()).toEqual(session.deriveMessages())
1740
+ const replayRecovered = await ctx.tools.execute({
1741
+ name: 'context_compression_retrieve',
1742
+ arguments: { ref: replayPublication?.ref, max_lines: 20 },
1743
+ callId: CallId('tailtrim-retrieve-replay'),
1744
+ signal: new AbortController().signal,
1745
+ agent: stubAgent(ctx, replay),
1746
+ })
1747
+ expect(replayRecovered).toEqual(recovered)
1748
+ })
1749
+
1750
+ it('audits TailTrim threshold, tokenizer, safety-group, and min-reclaim skip paths', async () => {
1751
+ async function runScenario(options: {
1752
+ readonly id: string
1753
+ readonly expectedReason: string
1754
+ readonly trigger: number
1755
+ readonly minReclaim: number
1756
+ readonly candidateTurns: number
1757
+ readonly model?: string
1758
+ }): Promise<void> {
1759
+ const ctx = new Context()
1760
+ activeContexts.push(ctx)
1761
+ await ctx.plugin(TestSettings).await()
1762
+ await ctx.plugin(SelectorHost).await()
1763
+ await ctx.plugin(SessionStore)
1764
+ await ctx.plugin(SystemPrompt)
1765
+ await ctx.plugin(ToolRuntime)
1766
+ await ctx.plugin(TokenMeter)
1767
+ const audit = captureAudit(ctx)
1768
+
1769
+ const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
1770
+ if (policy.version !== 3) throw new Error('TailTrim negative-path test requires policy v3')
1771
+ policy.fresh.enabled = false
1772
+ policy.aggregate.enabled = false
1773
+ policy.history.enabled = false
1774
+ policy.history.trigger = Math.max(policy.history.trigger, options.minReclaim)
1775
+ policy.history.keepRecentToolCalls = 0
1776
+ policy.history.keepRecentTokens = 0
1777
+ policy.history.minReclaim = options.minReclaim
1778
+ policy.tailTrim = { enabled: true, trigger: options.trigger }
1779
+ await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1780
+ profile: 'custom',
1781
+ custom: policy,
1782
+ })
1783
+ await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
1784
+
1785
+ const session = Session.create(SessionId(options.id))
1786
+ if (options.model !== undefined) {
1787
+ session.append('request/header', {
1788
+ reason: 'initial',
1789
+ header: canonicalHeader({ config: { provider: 'deepseek', model: options.model } }),
1790
+ })
1791
+ }
1792
+ for (let turn = 1; turn <= options.candidateTurns; turn++) {
1793
+ appendToolTurn(session, turn, `tailtrim negative ${options.id} `.repeat(200), true)
1794
+ }
1795
+ session.append('turn/start', { turn: options.candidateTurns + 1 })
1796
+
1797
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
1798
+
1799
+ expect(rewrites(audit.records()).filter(record => record.component === 'tail-trim')).toHaveLength(0)
1800
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1801
+ kind: 'component-evaluation',
1802
+ sessionId: String(session.id),
1803
+ component: 'tail-trim',
1804
+ status: 'skipped',
1805
+ reason: options.expectedReason,
1806
+ }))
1807
+ }
1808
+
1809
+ await runScenario({
1810
+ id: 'public-tailtrim-below-trigger',
1811
+ expectedReason: 'at-or-below-trigger',
1812
+ trigger: 1_000_000,
1813
+ minReclaim: 1,
1814
+ candidateTurns: 2,
1815
+ })
1816
+ await runScenario({
1817
+ id: 'public-tailtrim-tokenizer-unavailable',
1818
+ expectedReason: 'exact-tokenizer-unavailable',
1819
+ trigger: 1,
1820
+ minReclaim: 1,
1821
+ candidateTurns: 2,
1822
+ model: 'unsupported-public-model',
1823
+ })
1824
+ await runScenario({
1825
+ id: 'public-tailtrim-first-group-protected',
1826
+ expectedReason: 'no-safe-eligible-tool-group',
1827
+ trigger: 1,
1828
+ minReclaim: 1,
1829
+ candidateTurns: 1,
1830
+ })
1831
+ await runScenario({
1832
+ id: 'public-tailtrim-min-reclaim',
1833
+ expectedReason: 'no-safe-eligible-tool-group',
1834
+ trigger: 1,
1835
+ minReclaim: 1_000_000,
1836
+ candidateTurns: 2,
1837
+ })
1838
+ })
1839
+
1840
+ it('fails open after an orphan prune and remains appendable after JSON restart', async () => {
1841
+ const ctx = new Context()
1842
+ activeContexts.push(ctx)
1843
+ await ctx.plugin(TestSettings).await()
1844
+ await ctx.plugin(SelectorHost).await()
1845
+ await ctx.plugin(SessionStore)
1846
+ await ctx.plugin(InvariantRegistry)
1847
+ await ctx.plugin(RuntimeInvariant)
1848
+ await ctx.plugin(SystemPrompt)
1849
+ await ctx.plugin(ToolRuntime)
1850
+ await ctx.plugin(TokenMeter)
1851
+ const audit = captureAudit(ctx)
1852
+
1853
+ const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
1854
+ if (policy.version !== 3) throw new Error('orphan recovery test requires policy v3')
1855
+ policy.fresh.enabled = false
1856
+ policy.aggregate.enabled = false
1857
+ policy.history.enabled = false
1858
+ policy.history.keepRecentToolCalls = 0
1859
+ policy.history.keepRecentTokens = 0
1860
+ policy.history.minReclaim = 1
1861
+ policy.tailTrim = { enabled: true, trigger: 8 }
1862
+ await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
1863
+ profile: 'custom',
1864
+ custom: policy,
1865
+ })
1866
+ await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
1867
+ const session = ctx.sessions.create(SessionId('public-tailtrim-orphan-recovery'))
1868
+ appendToolTurn(session, 1, 'first completed group'.repeat(40), true)
1869
+ appendToolTurn(session, 2, 'orphan source remains intact'.repeat(400), true)
1870
+ session.append('turn/start', { turn: 3 })
1871
+ const beforeSurface = [...session.surface.nodes]
1872
+ const originalAppend = session.append.bind(session)
1873
+ const append = vi.spyOn(session, 'append').mockImplementation((type: string, ...args: unknown[]) => {
1874
+ const options = args[1] as { surfaceOp?: unknown } | undefined
1875
+ if (type === 'user/message' && typeof options?.surfaceOp === 'object') {
1876
+ throw new Error('fixture replacement append failure')
1877
+ }
1878
+ return Reflect.apply(
1879
+ originalAppend as (...values: unknown[]) => unknown,
1880
+ undefined,
1881
+ [type, ...args],
1882
+ ) as never
1883
+ })
1884
+
1885
+ expect(() => ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })).not.toThrow()
1886
+ append.mockRestore()
1887
+ expect(session.events.at(-1)?.type).toBe('compaction/prune')
1888
+ expect(session.surface.nodes).toEqual(beforeSurface)
1889
+ expect(rewrites(audit.records())).toHaveLength(0)
1890
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1891
+ kind: 'failure',
1892
+ operation: 'publication',
1893
+ component: 'tail-trim',
1894
+ manifestSeq: session.events.length - 1,
1895
+ }))
1896
+
1897
+ expect(() => session.append('turn/end', {
1898
+ turn: 3,
1899
+ reason: { kind: 'completed' },
1900
+ })).not.toThrow()
1901
+ const persisted = JSON.parse(JSON.stringify(session.events))
1902
+
1903
+ const resumedCtx = new Context()
1904
+ activeContexts.push(resumedCtx)
1905
+ await resumedCtx.plugin(SessionStore)
1906
+ await resumedCtx.plugin(InvariantRegistry)
1907
+ await resumedCtx.plugin(RuntimeInvariant)
1908
+ const resumed = resumedCtx.sessions.create(session.id, { seed: persisted })
1909
+ expect(resumed.surface.nodes).toEqual(beforeSurface)
1910
+ expect(() => resumed.append('turn/start', { turn: 4 })).not.toThrow()
1911
+ })
1912
+
1913
+ it('still rejects a malformed adjacent replacement instead of treating it as an orphan', async () => {
1914
+ const ctx = new Context()
1915
+ activeContexts.push(ctx)
1916
+ await ctx.plugin(SessionStore)
1917
+ await ctx.plugin(InvariantRegistry)
1918
+ await ctx.plugin(RuntimeInvariant)
1919
+ const session = ctx.sessions.create(SessionId('public-malformed-companion'))
1920
+ const source = appendToolTurn(session, 1, 'source remains intact', false)
1921
+ session.append('compaction/prune', {
1922
+ shadowedRange: { start: source.resultSeq, end: source.resultSeq },
1923
+ shadowedSeqs: [source.resultSeq],
1924
+ shadowedTokenCount: 1,
1925
+ })
1926
+ const result = session.events[source.resultSeq]
1927
+ if (result?.type !== 'tool/result') throw new Error('missing source result')
1928
+ const before = session.seq
1929
+
1930
+ expect(() => session.append('tool/result', result.data, {
1931
+ surfaceOp: { op: 'replace', start: source.assistantSeq, end: source.assistantSeq },
1932
+ sourceEventSeqs: [source.resultSeq],
1933
+ })).toThrow(/sourceEventSeqs|does not replace compaction\/prune range/)
1934
+ expect(session.seq).toBe(before)
1935
+ })
1936
+
1937
+ it('observes committed Native auto-compact separately from plugin rewrites', async () => {
1938
+ const ctx = await runtimeContext()
1939
+ const audit = captureAudit(ctx)
1940
+ await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
1941
+ const session = Session.create(SessionId('public-native-auto-audit'))
1942
+ const summary = session.append('compaction/summary', {
1943
+ compactionId: CompactionId('public-native-auto'),
1944
+ summary: [{ type: 'text', text: 'summary' }],
1945
+ shadowedRange: { start: 0, end: 0 },
1946
+ shadowedSeqs: [],
1947
+ shadowedTokenCount: 321,
1948
+ provider: 'deepseek',
1949
+ model: MODEL,
1950
+ })
1951
+
1952
+ ctx.emit('session/event', session, summary)
1953
+
1954
+ expect(rewrites(audit.records())).toHaveLength(0)
1955
+ expect(audit.records()).toContainEqual(expect.objectContaining({
1956
+ kind: 'native-auto-compact',
1957
+ manifestEventType: 'compaction/summary',
1958
+ manifestSeq: summary.seq,
1959
+ provider: 'deepseek',
1960
+ model: MODEL,
1961
+ tokensBefore: 321,
1962
+ tokensAfter: null,
1963
+ }))
1964
+ })
1965
+
1966
+ it('triggers Native auto-compact through the real AgentLoop pre-step boundary', async () => {
1967
+ const ctx = new Context()
1968
+ activeContexts.push(ctx)
1969
+ await mountAgentLoopTestDependencies(ctx)
1970
+ await ctx.plugin(AgentLoop, { agents: [] })
1971
+ await ctx.plugin(TokenMeter)
1972
+ const audit = captureAudit(ctx)
1973
+ ctx.llm.registerAdapter(
1974
+ ['deepseek'],
1975
+ new NativeSummaryAdapter([
1976
+ 'first native turn complete',
1977
+ 'native compact summary',
1978
+ 'second native turn complete',
1979
+ ], 1_000),
1980
+ )
1981
+ await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
1982
+ void new BasicCompactionEngine(ctx, {
1983
+ auto: true,
1984
+ thresholdRatio: 0.3,
1985
+ retainTokens: 0,
1986
+ maxTokens: 100,
1987
+ compactionRetries: 0,
1988
+ })
1989
+ const agent = ctx.agentLoop.create(SessionId('public-native-auto-real'), {
1990
+ provider: 'deepseek',
1991
+ model: MODEL,
1992
+ })
1993
+
1994
+ agent.followup(createUserMessage({
1995
+ content: [{ type: 'text', text: 'native pressure evidence '.repeat(500) }],
1996
+ source: { kind: 'user' },
1997
+ }))
1998
+ await agent.whenIdle()
1999
+ agent.followup(createUserMessage({
2000
+ content: [{ type: 'text', text: 'continue after native compaction' }],
2001
+ source: { kind: 'user' },
2002
+ }))
2003
+ await agent.whenIdle()
2004
+
2005
+ expect(rewrites(audit.records())).toHaveLength(0)
2006
+ const summary = agent.session.events.findLast(event => event.type === 'compaction/summary')
2007
+ expect(summary?.type).toBe('compaction/summary')
2008
+ expect(audit.records()).toContainEqual(expect.objectContaining({
2009
+ kind: 'native-auto-compact',
2010
+ manifestEventType: 'compaction/summary',
2011
+ manifestSeq: summary?.seq,
2012
+ provider: 'deepseek',
2013
+ model: MODEL,
2014
+ tokensBefore: expect.any(Number),
2015
+ tokensAfter: null,
2016
+ }))
2017
+ })
2018
+
2019
+ it('runs Fresh, Aggregate, History, TailTrim and Native in one non-isolated audited session', async () => {
2020
+ const ctx = new Context()
2021
+ activeContexts.push(ctx)
2022
+ await ctx.plugin(TestSettings).await()
2023
+ await ctx.plugin(SelectorHost).await()
2024
+ await ctx.plugin(LlmRuntime)
2025
+ await ctx.plugin(SessionStore)
2026
+ await ctx.plugin(SystemPrompt)
2027
+ await ctx.plugin(ToolRuntime)
2028
+ await ctx.plugin(TokenMeter)
2029
+ const audit = captureAudit(ctx)
2030
+
2031
+ const policy = structuredClone(DEFAULT_CUSTOM_COMPRESSION_POLICY) as CustomCompressionPolicy
2032
+ if (policy.version !== 3) throw new Error('full-pipeline public E2E requires policy v3')
2033
+ policy.fresh = { enabled: true, trigger: 512, target: 256 }
2034
+ policy.aggregate = { enabled: true, trigger: 1_000, target: 400 }
2035
+ policy.history = {
2036
+ enabled: true,
2037
+ // The strict required-reclaim gate demands one batch pull tool tokens
2038
+ // back under this trigger: it must sit above the large protected tail
2039
+ // (so the batch is reachable) while the residual total stays far above
2040
+ // the auto-compact threshold (half the measured total) afterwards.
2041
+ trigger: 7_800,
2042
+ keepRecentToolCalls: 0,
2043
+ keepRecentTokens: 1,
2044
+ minReclaim: 1,
2045
+ }
2046
+ policy.prefixPolicy = 'pressure-break'
2047
+ policy.tailTrim = { enabled: true, trigger: 8 }
2048
+ await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
2049
+ profile: 'custom',
2050
+ custom: policy,
2051
+ })
2052
+ await ctx.plugin(ToolResultPruner, { profile: 'off' }).await()
2053
+
2054
+ const session = ctx.sessions.create(SessionId('public-full-pipeline-e2e'))
2055
+ appendToolTurn(session, 1, 'fresh full pipeline '.repeat(600), false, 'run Fresh')
2056
+ ctx.toolResultPruner.pruneSession(session, {
2057
+ stage: 'fresh',
2058
+ freshTurn: 1,
2059
+ freshStep: 1,
2060
+ })
2061
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
2062
+
2063
+ appendToolBatchTurn(
2064
+ session,
2065
+ 2,
2066
+ Array.from({ length: 30 }, (_, index) => `aggregate-${String(index)} `.repeat(20)),
2067
+ false,
2068
+ 'run Aggregate',
2069
+ )
2070
+ ctx.toolResultPruner.pruneSession(session, {
2071
+ stage: 'fresh',
2072
+ freshTurn: 2,
2073
+ freshStep: 1,
2074
+ })
2075
+ session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
2076
+ // Leave one old, still-original result for the pressure-stage History
2077
+ // reducer, then add a newer working-set result that remains protected.
2078
+ appendToolTurn(session, 3, 'history full pipeline '.repeat(600), true, 'run History')
2079
+ appendToolTurn(session, 4, 'recent protected working-set context '.repeat(1200), true, 'retain recent context')
2080
+ session.append('turn/start', { turn: 5 })
2081
+ ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
2082
+
2083
+ const beforeNative = ctx.tokenMeter.measure(session).totalTokens
2084
+ expect(beforeNative).toBeGreaterThan(2)
2085
+ ctx.llm.registerAdapter(
2086
+ ['deepseek'],
2087
+ new NativeSummaryAdapter(['full-pipeline native summary'], beforeNative),
2088
+ )
2089
+ void new BasicCompactionEngine(ctx, {
2090
+ auto: true,
2091
+ thresholdRatio: 0.5,
2092
+ retainTokens: 0,
2093
+ maxTokens: 100,
2094
+ compactionRetries: 0,
2095
+ })
2096
+ const signal = new AbortController().signal
2097
+ const decision = await agentEvents(ctx, stubAgent(ctx, session)).waterfall(
2098
+ 'agent/pre-step',
2099
+ { messages: [], turn: 5, step: 1, signal },
2100
+ () => Promise.resolve({ kind: 'enter' as const, messages: [] }),
2101
+ )
2102
+ expect(decision).toEqual({ kind: 'enter', messages: [] })
2103
+ const summary = session.events.findLast(event => event.type === 'compaction/summary')
2104
+ expect(summary?.type).toBe('compaction/summary')
2105
+
2106
+ const records = audit.records()
2107
+ const rewriteRecords = rewrites(records)
2108
+ const components = rewriteRecords.map(record => record.component)
2109
+ expect(components).toContain('fresh')
2110
+ expect(components).toContain('aggregate')
2111
+ expect(components).toContain('history')
2112
+ expect(components).toContain('tail-trim')
2113
+ expect(records).toContainEqual(expect.objectContaining({
2114
+ kind: 'native-auto-compact',
2115
+ manifestSeq: summary?.seq,
2116
+ tokensBefore: expect.any(Number),
2117
+ }))
2118
+
2119
+ const firstIndex = (component: CompressionRewriteAuditRecord['component']) =>
2120
+ records.findIndex(record => record.kind === 'rewrite' && record.component === component)
2121
+ expect(firstIndex('fresh')).toBeLessThan(firstIndex('aggregate'))
2122
+ expect(firstIndex('aggregate')).toBeLessThan(firstIndex('history'))
2123
+ expect(firstIndex('history')).toBeLessThan(firstIndex('tail-trim'))
2124
+ expect(records.findIndex(record => record.kind === 'native-auto-compact'))
2125
+ .toBeGreaterThan(firstIndex('tail-trim'))
2126
+ expect(session.events.some(event => event.type === ('compaction/group-trim' as string))).toBe(false)
2127
+ })
2128
+
2129
+ it('compresses text tool results exactly in a vision session that also carries a user image', async () => {
2130
+ const ctx = await runtimeContext()
2131
+ const audit = captureAudit(ctx)
2132
+ await ctx.plugin(ToolResultPruner, {
2133
+ profile: 'balanced',
2134
+ freshTriggerTokens: 1_000_000,
2135
+ freshTargetTokens: 900_000,
2136
+ aggregateTriggerTokens: 100,
2137
+ aggregateTargetTokens: 64,
2138
+ historyTriggerTokens: 600,
2139
+ historyKeepRecentToolCalls: 0,
2140
+ historyKeepRecentTokens: 1,
2141
+ historyMinReclaimTokens: 1,
2142
+ }).await()
2143
+ const session = Session.create(SessionId('public-vision-text-session'))
2144
+ session.append('turn/start', { turn: 1 })
2145
+ session.append('request/header', {
2146
+ reason: 'initial',
2147
+ header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
2148
+ })
2149
+ session.append('user/message', createUserMessage({
2150
+ content: [
2151
+ imageBlock(640, 480),
2152
+ { type: 'text', text: 'describe the attachment and run the tools' },
2153
+ ],
2154
+ source: { kind: 'user' },
2155
+ }), { surfaceOp: 'append' })
2156
+ session.append('step/start', { turn: 1, step: 1 })
2157
+ session.append('assistant/message', {
2158
+ turn: 1,
2159
+ step: 1,
2160
+ message: createMessage({
2161
+ role: 'assistant',
2162
+ content: [{ type: 'tool-call', id: CallId('vision-call-1'), name: 'bash', arguments: '{}' }],
2163
+ source: { kind: 'model', provider: 'deepseek', model: VISION_MODEL },
2164
+ }),
2165
+ }, { surfaceOp: 'append' })
2166
+ session.append('tool/call', { turn: 1, step: 1, callId: CallId('vision-call-1'), name: 'bash', arguments: '{}' })
2167
+ session.append('tool/result', {
2168
+ turn: 1,
2169
+ step: 1,
2170
+ message: createToolResultMessage({
2171
+ callId: CallId('vision-call-1'),
2172
+ content: [{ type: 'text', text: 'vision fresh evidence '.repeat(1_000) }],
2173
+ isError: false,
2174
+ }),
2175
+ }, { surfaceOp: 'append' })
2176
+ session.append('step/end', { turn: 1, step: 1 })
2177
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
2178
+ // One older text result outside the fresh coordinates keeps History above
2179
+ // its trigger after Fresh has already shrunk the turn-1 result.
2180
+ appendToolTurn(session, 2, 'vision history evidence '.repeat(300), true, undefined, 'deepseek', VISION_MODEL)
2181
+ appendToolTurn(session, 3, 'recent protected result', true, undefined, 'deepseek', VISION_MODEL)
2182
+ session.append('turn/start', { turn: 4 })
2183
+
2184
+ const fresh = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
2185
+ const pressure = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
2186
+
2187
+ expect(fresh.pruned).toHaveLength(1)
2188
+ expect(pressure.pruned.length).toBeGreaterThanOrEqual(1)
2189
+ const visionRewrites = rewrites(audit.records())
2190
+ .filter(record => record.sessionId === String(session.id))
2191
+ expect(visionRewrites.length).toBeGreaterThanOrEqual(2)
2192
+ for (const record of visionRewrites) {
2193
+ expect(record.tokenizerId).toBe('deepseek-ai/DeepSeek-V4-Flash-Vision-Exp')
2194
+ expect(record.tokenizerRevision).toBe('6821d6ad3681a4b137b066b76094fa82ebd0a380')
2195
+ }
2196
+ // Fresh is disabled by its trigger; Aggregate is the pre-compression stage
2197
+ // that lands on the vision route for the oversized new result.
2198
+ expect(visionRewrites.some(record => record.component === 'fresh')).toBe(false)
2199
+ expect(visionRewrites.some(record => record.component === 'aggregate')).toBe(true)
2200
+ expect(visionRewrites.some(record => record.component === 'history')).toBe(true)
2201
+ })
2202
+
2203
+ it('keeps an image-bearing tool result fail-open in a vision session', async () => {
2204
+ const ctx = await runtimeContext()
2205
+ const audit = captureAudit(ctx)
2206
+ await ctx.plugin(ToolResultPruner, {
2207
+ profile: 'balanced',
2208
+ freshTriggerTokens: 10,
2209
+ freshTargetTokens: 8,
2210
+ aggregateTriggerTokens: 10,
2211
+ aggregateTargetTokens: 8,
2212
+ historyTriggerTokens: 10,
2213
+ historyKeepRecentToolCalls: 0,
2214
+ historyKeepRecentTokens: 1,
2215
+ historyMinReclaimTokens: 1,
2216
+ }).await()
2217
+ const session = Session.create(SessionId('public-vision-image-tool-result'))
2218
+ session.append('turn/start', { turn: 1 })
2219
+ session.append('request/header', {
2220
+ reason: 'initial',
2221
+ header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
2222
+ })
2223
+ session.append('step/start', { turn: 1, step: 1 })
2224
+ session.append('assistant/message', {
2225
+ turn: 1,
2226
+ step: 1,
2227
+ message: createMessage({
2228
+ role: 'assistant',
2229
+ content: [{ type: 'tool-call', id: CallId('vision-image-call'), name: 'screenshot', arguments: '{}' }],
2230
+ source: { kind: 'model', provider: 'deepseek', model: VISION_MODEL },
2231
+ }),
2232
+ }, { surfaceOp: 'append' })
2233
+ session.append('tool/call', { turn: 1, step: 1, callId: CallId('vision-image-call'), name: 'screenshot', arguments: '{}' })
2234
+ const imageResult = session.append('tool/result', {
2235
+ turn: 1,
2236
+ step: 1,
2237
+ message: createToolResultMessage({
2238
+ callId: CallId('vision-image-call'),
2239
+ content: [
2240
+ { type: 'text', text: 'screenshot captured' },
2241
+ imageBlock(800, 600),
2242
+ ],
2243
+ isError: false,
2244
+ }),
2245
+ }, { surfaceOp: 'append' })
2246
+ session.append('step/end', { turn: 1, step: 1 })
2247
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
2248
+ session.append('turn/start', { turn: 2 })
2249
+
2250
+ const fresh = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
2251
+ const pressure = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
2252
+
2253
+ expect(fresh.pruned).toHaveLength(0)
2254
+ expect(pressure.pruned).toHaveLength(0)
2255
+ // The original image-bearing result stays on the surface untouched.
2256
+ const original = session.events[imageResult.seq]
2257
+ expect(original?.type).toBe('tool/result')
2258
+ expect(audit.records()).toContainEqual(expect.objectContaining({
2259
+ kind: 'component-evaluation',
2260
+ sessionId: String(session.id),
2261
+ component: 'fresh',
2262
+ status: 'skipped',
2263
+ reason: 'exact-tokenizer-unavailable',
2264
+ }))
2265
+ expect(audit.records()).toContainEqual(expect.objectContaining({
2266
+ kind: 'component-evaluation',
2267
+ sessionId: String(session.id),
2268
+ component: 'history',
2269
+ status: 'skipped',
2270
+ reason: 'exact-tokenizer-unavailable',
2271
+ }))
2272
+ })
2273
+
2274
+ it('counts image nodes as estimates while retaining exact text-only siblings', async () => {
2275
+ const ctx = await runtimeContext()
2276
+ const session = Session.create(SessionId('public-vision-image-surface'))
2277
+ session.append('turn/start', { turn: 1 })
2278
+ session.append('request/header', {
2279
+ reason: 'initial',
2280
+ header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
2281
+ })
2282
+ const userMessage = session.append('user/message', createUserMessage({
2283
+ content: [
2284
+ { type: 'text', text: 'look at this' },
2285
+ imageBlock(640, 480),
2286
+ { type: 'text', text: 'and describe it' },
2287
+ ],
2288
+ source: { kind: 'user' },
2289
+ }), { surfaceOp: 'append' })
2290
+ const textOnly = session.append('user/message', createUserMessage({
2291
+ content: [{ type: 'text', text: 'still exactly measurable' }],
2292
+ source: { kind: 'user' },
2293
+ }), { surfaceOp: 'append' })
2294
+
2295
+ const view = measureForCompaction(ctx, session)
2296
+
2297
+ // The exact request expansion is not publicly verifiable, so a mixed
2298
+ // text/image node carries a bounded estimate and cannot authorize an exact
2299
+ // rewrite proof.
2300
+ const node = view.measuredNodes.find(entry => entry.seq === userMessage.seq)
2301
+ expect(node?.count).toMatchObject({
2302
+ kind: 'tokenizer-estimate',
2303
+ estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
2304
+ estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
2305
+ })
2306
+ if (node?.count.kind !== 'tokenizer-estimate') throw new Error('image node must carry an estimate')
2307
+ expect(node.count.upperBoundTokens).toBeGreaterThanOrEqual(node.count.tokens)
2308
+ expect(view.currentSurface.kind).toBe('tokenizer-estimate')
2309
+ // Text-only siblings keep their exact counts.
2310
+ expect(view.measuredNodes.find(entry => entry.seq === textOnly.seq)?.count).toMatchObject({
2311
+ kind: 'exact-tokenizer',
2312
+ tokenizerId: 'deepseek-ai/DeepSeek-V4-Flash-Vision-Exp',
2313
+ })
2314
+ // The official arithmetic is attached as an INTRINSIC diagnostic (the
2315
+ // padding extremes on intrinsic dimensions), explicitly not a request
2316
+ // bound: the adapter may still re-project the image.
2317
+ const grid = deepSeekVisionImageGrid(640, 480)
2318
+ expect(node.intrinsicImageBlockEstimate).toMatchObject({
2319
+ paddingMinimumTokens: deepSeekVisionImageBlockTokens(grid.nLlmH, grid.nLlmW, 3),
2320
+ paddingMaximumTokens: deepSeekVisionImageBlockTokens(grid.nLlmH, grid.nLlmW, 0),
2321
+ })
2322
+ expect(view.intrinsicImageBlockEstimateTokens).toBe(node.intrinsicImageBlockEstimate?.paddingMinimumTokens)
2323
+ })
2324
+
2325
+ it('estimates over-budget image metadata instead of making the surface unavailable', async () => {
2326
+ const ctx = await runtimeContext()
2327
+ const session = Session.create(SessionId('public-vision-image-over-budget'))
2328
+ session.append('turn/start', { turn: 1 })
2329
+ session.append('request/header', {
2330
+ reason: 'initial',
2331
+ header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
2332
+ })
2333
+ const userMessage = session.append('user/message', createUserMessage({
2334
+ content: [imageBlock(4096, 4096)],
2335
+ source: { kind: 'user' },
2336
+ }), { surfaceOp: 'append' })
2337
+
2338
+ const view = measureForCompaction(ctx, session)
2339
+
2340
+ const node = view.measuredNodes.find(entry => entry.seq === userMessage.seq)
2341
+ expect(node?.count).toMatchObject({
2342
+ kind: 'tokenizer-estimate',
2343
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
2344
+ })
2345
+ expect(view.currentSurface.kind).toBe('tokenizer-estimate')
2346
+ })
2347
+
2348
+ it('keeps empty content nodes exact instead of fail-opening the surface', async () => {
2349
+ const ctx = await runtimeContext()
2350
+ const session = Session.create(SessionId('public-empty-content-node'))
2351
+ session.append('turn/start', { turn: 1 })
2352
+ session.append('request/header', {
2353
+ reason: 'initial',
2354
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
2355
+ })
2356
+ const empty = session.append('user/message', createUserMessage({
2357
+ content: [],
2358
+ source: { kind: 'user' },
2359
+ }), { surfaceOp: 'append' })
2360
+ const present = session.append('user/message', createUserMessage({
2361
+ content: [{ type: 'text', text: 'still measurable' }],
2362
+ source: { kind: 'user' },
2363
+ }), { surfaceOp: 'append' })
2364
+
2365
+ const view = measureForCompaction(ctx, session)
2366
+
2367
+ expect(view.measuredNodes.find(node => node.seq === empty.seq)?.count).toMatchObject({
2368
+ kind: 'exact-tokenizer',
2369
+ tokens: 0,
2370
+ })
2371
+ expect(view.measuredNodes.find(node => node.seq === present.seq)?.count).toMatchObject({
2372
+ kind: 'exact-tokenizer',
2373
+ })
2374
+ expect(view.currentSurface.kind).toBe('exact-tokenizer')
2375
+ })
2376
+
2377
+ it('uses the fixed image-token fallback for malformed metadata', async () => {
2378
+ const ctx = await runtimeContext()
2379
+ const session = Session.create(SessionId('public-vision-image-malformed-dims'))
2380
+ session.append('turn/start', { turn: 1 })
2381
+ session.append('request/header', {
2382
+ reason: 'initial',
2383
+ header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
2384
+ })
2385
+ const userMessage = session.append('user/message', createUserMessage({
2386
+ content: [imageBlock(640.5, 480)],
2387
+ source: { kind: 'user' },
2388
+ }), { surfaceOp: 'append' })
2389
+ const zeroSize = session.append('user/message', createUserMessage({
2390
+ content: [imageBlock(0, 480)],
2391
+ source: { kind: 'user' },
2392
+ }), { surfaceOp: 'append' })
2393
+
2394
+ const view = measureForCompaction(ctx, session)
2395
+
2396
+ expect(view.measuredNodes.find(node => node.seq === userMessage.seq)?.count).toEqual({
2397
+ kind: 'tokenizer-estimate',
2398
+ tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
2399
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
2400
+ estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
2401
+ estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
2402
+ })
2403
+ expect(view.measuredNodes.find(node => node.seq === zeroSize.seq)?.count).toEqual({
2404
+ kind: 'tokenizer-estimate',
2405
+ tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS,
2406
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens,
2407
+ estimatorId: DEEPSEEK_VISION_IMAGE_ESTIMATOR.id,
2408
+ estimatorRevision: DEEPSEEK_VISION_IMAGE_ESTIMATOR.revision,
2409
+ })
2410
+ expect(view.currentSurface).toMatchObject({
2411
+ kind: 'tokenizer-estimate',
2412
+ tokens: DEEPSEEK_VISION_DEFAULT_IMAGE_TOKENS * 2,
2413
+ upperBoundTokens: DEEPSEEK_VISION_PROJECTION.visionMaxNTokens * 2,
2414
+ })
2415
+ })
2416
+
2417
+ it('keeps image content unavailable for non-vision models', async () => {
2418
+ const ctx = await runtimeContext()
2419
+ const session = Session.create(SessionId('public-text-model-image-node'))
2420
+ session.append('turn/start', { turn: 1 })
2421
+ session.append('request/header', {
2422
+ reason: 'initial',
2423
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
2424
+ })
2425
+ const userMessage = session.append('user/message', createUserMessage({
2426
+ content: [imageBlock(640, 480)],
2427
+ source: { kind: 'user' },
2428
+ }), { surfaceOp: 'append' })
2429
+
2430
+ const view = measureForCompaction(ctx, session)
2431
+
2432
+ expect(view.measuredNodes.find(entry => entry.seq === userMessage.seq)?.count).toMatchObject({
2433
+ kind: 'unavailable',
2434
+ })
2435
+ })
2436
+
2437
+ it('keeps the vision image estimator unavailable behind a foreign provider', async () => {
2438
+ const ctx = await runtimeContext()
2439
+ const session = Session.create(SessionId('public-foreign-provider-vision-image-node'))
2440
+ session.append('turn/start', { turn: 1 })
2441
+ session.append('request/header', {
2442
+ reason: 'initial',
2443
+ header: canonicalHeader({ config: { provider: 'openai', model: VISION_MODEL } }),
2444
+ })
2445
+ const userMessage = session.append('user/message', createUserMessage({
2446
+ content: [imageBlock(640, 480)],
2447
+ source: { kind: 'user' },
2448
+ }), { surfaceOp: 'append' })
2449
+
2450
+ const view = measureForCompaction(ctx, session)
2451
+
2452
+ expect(view.measuredNodes.find(entry => entry.seq === userMessage.seq)?.count).toMatchObject({
2453
+ kind: 'unavailable',
2454
+ })
2455
+ expect(view.currentSurface).toMatchObject({ kind: 'unavailable' })
2456
+ })
2457
+
2458
+ it('never TailTrims a tool group whose results carry images', async () => {
2459
+ const ctx = await runtimeContext()
2460
+ await ctx.plugin(TestSettings).await()
2461
+ await ctx.plugin(SelectorHost).await()
2462
+ const audit = captureAudit(ctx)
2463
+ await ctx.settings.update(settingsNamespace(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
2464
+ profile: 'custom',
2465
+ custom: {
2466
+ version: 3,
2467
+ unit: 'tokens',
2468
+ fresh: { enabled: true, trigger: 1_000_000, target: 900_000 },
2469
+ aggregate: { enabled: true, trigger: 1_000_000, target: 900_000 },
2470
+ history: { enabled: false, trigger: 900_000, keepRecentToolCalls: 0, keepRecentTokens: 0, minReclaim: 1 },
2471
+ prefixPolicy: 'preserve',
2472
+ tailTrim: { enabled: true, trigger: 10 },
2473
+ },
2474
+ })
2475
+ await ctx.plugin(ToolResultPruner, { profile: 'custom' }).await()
2476
+ const session = Session.create(SessionId('public-vision-tailtrim-image-group'))
2477
+ session.append('turn/start', { turn: 1 })
2478
+ session.append('request/header', {
2479
+ reason: 'initial',
2480
+ header: canonicalHeader({ config: { provider: 'deepseek', model: VISION_MODEL } }),
2481
+ })
2482
+ session.append('step/start', { turn: 1, step: 1 })
2483
+ session.append('assistant/message', {
2484
+ turn: 1,
2485
+ step: 1,
2486
+ message: createMessage({
2487
+ role: 'assistant',
2488
+ content: [{ type: 'tool-call', id: CallId('vision-tailtrim-call'), name: 'screenshot', arguments: '{}' }],
2489
+ source: { kind: 'model', provider: 'deepseek', model: VISION_MODEL },
2490
+ }),
2491
+ }, { surfaceOp: 'append' })
2492
+ session.append('tool/call', { turn: 1, step: 1, callId: CallId('vision-tailtrim-call'), name: 'screenshot', arguments: '{}' })
2493
+ session.append('tool/result', {
2494
+ turn: 1,
2495
+ step: 1,
2496
+ message: createToolResultMessage({
2497
+ callId: CallId('vision-tailtrim-call'),
2498
+ content: [
2499
+ { type: 'text', text: 'screenshot captured for tail trim' },
2500
+ imageBlock(320, 240),
2501
+ ],
2502
+ isError: false,
2503
+ }),
2504
+ }, { surfaceOp: 'append' })
2505
+ session.append('step/end', { turn: 1, step: 1 })
2506
+ session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
2507
+ session.append('turn/start', { turn: 2 })
2508
+
2509
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
2510
+
2511
+ expect(result.pruned).toHaveLength(0)
2512
+ expect(rewrites(audit.records()).some(record =>
2513
+ record.sessionId === String(session.id) && record.component === 'tail-trim')).toBe(false)
2514
+ expect(session.events.some(event =>
2515
+ event.type === 'tool/result'
2516
+ && event.data.message.content.some(block => block.type === 'tool-result'
2517
+ && block.content.some(inner => inner.type === 'image')))).toBe(true)
2518
+ // Either fail-open gate is acceptable: the protected-set scan refuses the
2519
+ // image-bearing candidate, and the group scan independently refuses it.
2520
+ const tailTrimSkips = audit.records().filter((record): record is Extract<CompressionAuditRecord, { kind: 'component-evaluation' }> =>
2521
+ record.kind === 'component-evaluation'
2522
+ && record.sessionId === String(session.id)
2523
+ && record.component === 'tail-trim'
2524
+ && record.status === 'skipped')
2525
+ expect(tailTrimSkips.length).toBeGreaterThan(0)
2526
+ expect(tailTrimSkips.every(record =>
2527
+ record.reason === 'no-safe-eligible-tool-group'
2528
+ || record.reason === 'exact-tokenizer-unavailable-in-protected-set'
2529
+ || record.reason === 'exact-tokenizer-unavailable')).toBe(true)
2530
+ })
2531
+ })