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,2144 @@
1
+ /**
2
+ * Replay-safe, model-free context-compression selector for tool results.
3
+ *
4
+ * Standard profiles never rewrite ordinary Assistant prose. The only durable
5
+ * replacements emitted here are content-only `tool/result` rewrites whose
6
+ * full source remains in the append-only Session log.
7
+ *
8
+ * @module dsh-context-compression-improved-runtime
9
+ */
10
+
11
+ import { Service } from '@deepseek-ai/cordis'
12
+ import type { Context } from '@deepseek-ai/cordis'
13
+ import z from '@deepseek-ai/schemastery'
14
+ import { createUserMessage, freezeMessage } from '@deepseek-ai/dsh-llm'
15
+ import type { ContentBlock, UserMessage } from '@deepseek-ai/dsh-llm'
16
+ import type { Session, SessionEvent, ToolResultMessage } from '@deepseek-ai/dsh-session'
17
+ import type {} from '@deepseek-ai/dsh-agent'
18
+ import type {} from '@deepseek-ai/dsh-compaction'
19
+ import type {} from '@deepseek-ai/dsh-settings'
20
+ import type {
21
+ CompactionTokenView,
22
+ ObservedPromptUsage,
23
+ TokenCount,
24
+ } from './runtime/measurement.ts'
25
+ import { measureForCompaction } from './runtime/measurement.ts'
26
+ import { sessionEvents } from './runtime/session-events.ts'
27
+ import { countExactCanonicalTextFields } from './runtime/token-count.ts'
28
+ import type {} from '@deepseek-ai/dsh-tools'
29
+ import {
30
+ tailTrimMessage,
31
+ tailTrimRef,
32
+ tailTrimStub,
33
+ } from './runtime/tail-trim.ts'
34
+ import { installContextCompressionRetrieve } from './runtime/retrieve.ts'
35
+ import type { PrunerState } from './pruner/state.ts'
36
+ import { countOmittedLines, CAPACITY_PRESSURE_RATIO } from './pruner/tuning.ts'
37
+ import type { ToolCallInfo, SnapshotCandidate, PlannedReplacement, HistoryPlanOutcome } from './pruner/types.ts'
38
+ import {
39
+ onlyTextBlock,
40
+ onlyTextBlocks,
41
+ countToolContent,
42
+ exactTokens,
43
+ sameProviderMeasurementKey,
44
+ unavailableCount,
45
+ recoveryMarker,
46
+ summarize,
47
+ emptyResult,
48
+ measureContent,
49
+ pressureCost,
50
+ nativePruneContent,
51
+ } from './pruner/content.ts'
52
+ import {
53
+ hasOpenTurn,
54
+ rootToolResultSeq,
55
+ sourceRef as sourceRefFn,
56
+ latestCompletedToolStep,
57
+ routeAuditFact,
58
+ tokenizerAuditFact,
59
+ isError,
60
+ historyOutcome,
61
+ } from './pruner/session.ts'
62
+ import { buildLocatorBlock, findCompactionTrace } from './runtime/tokenpilot/locator.ts'
63
+ import { clusterOmittedLines, isSupersededRead, toolCallPath } from './runtime/tokenpilot/read-state.ts'
64
+ import {
65
+ Estimator,
66
+ backoffCooldownMs,
67
+ buildEstimatorSystemPrompt,
68
+ buildEstimatorUserPrompt,
69
+ isCoolingDown,
70
+ parseEstimatorAnswer,
71
+ type EstimatorFailures,
72
+ type EstimatorSample,
73
+ } from './runtime/tokenpilot/estimator.ts'
74
+
75
+ import {
76
+ DedupeTable,
77
+ dedupeHash,
78
+ dedupePlaceholder,
79
+ flattenPlainText,
80
+ } from './runtime/tokenpilot/dedup.ts'
81
+ import {
82
+ codePointLength,
83
+ CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
84
+ ContextCompressionSettingsSchema,
85
+ parseContextCompressionSettings,
86
+ DEFAULTS,
87
+ PRUNE_MARKER,
88
+ resolveConfig,
89
+ resolvePolicy,
90
+ } from './runtime/config.ts'
91
+ import {
92
+ historicalPlaceholder,
93
+ reduceFreshToolResult,
94
+ verifyReduction,
95
+ } from './runtime/reducers.ts'
96
+ import type {
97
+ CompressionPolicy,
98
+ ContextCompressionSettings,
99
+ HistoryMode,
100
+ PrunedEntry,
101
+ PruneResult,
102
+ PruneSessionOptions,
103
+ PruneStage,
104
+ ToolResultPruneConfig,
105
+ } from './runtime/types.ts'
106
+ import { COMPRESSION_PROFILES } from './runtime/types.ts'
107
+ import {
108
+ decideConservativeAdaptive,
109
+ deriveAdaptiveTokenBounds,
110
+ } from './runtime/adaptive-cost.ts'
111
+ import {
112
+ DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION,
113
+ priceOfficialDeepSeekUsage,
114
+ resolveOfficialDeepSeekPrice,
115
+ } from './runtime/deepseek-official-pricing.ts'
116
+ import { emitCompressionAudit } from './runtime/audit.ts'
117
+ import { assertNever, deepFreeze } from './runtime/value.ts'
118
+ import type {
119
+ CompressionAuditComponent,
120
+ CompressionAuditEvaluationStatus,
121
+ } from './runtime/audit.ts'
122
+
123
+ export {
124
+ codePointLength,
125
+ CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
126
+ ContextCompressionSettingsSchema,
127
+ parseContextCompressionSettings,
128
+ AUTO_COMPACT_THRESHOLD_LIMITS,
129
+ DEFAULTS,
130
+ isCompressionProfile,
131
+ isValidAutoCompactThresholdPercent,
132
+ PRUNE_MARKER,
133
+ resolveConfig,
134
+ resolvePolicy,
135
+ } from './runtime/config.ts'
136
+ export {
137
+ CustomCompressionPolicySchema,
138
+ DEFAULT_CUSTOM_COMPRESSION_POLICY,
139
+ resolveCustomPolicy,
140
+ } from './runtime/custom-policy.ts'
141
+ export type { CustomPolicyResolutionOptions } from './runtime/custom-policy.ts'
142
+ export { historicalPlaceholder, normalizeTerminalText, reduceFreshToolResult, verifyReduction } from './runtime/reducers.ts'
143
+ export { measureForCompaction } from './runtime/measurement.ts'
144
+ export type {
145
+ CompactionTokenView,
146
+ MeasuredTokenSurfaceNode,
147
+ } from './runtime/measurement.ts'
148
+ export type {
149
+ AutoCompactSettings,
150
+ CodeSkeletonSettings,
151
+ CompressionPolicy,
152
+ CompressionProfile,
153
+ CustomCompressionBudget,
154
+ CustomCompressionPolicy,
155
+ CustomCompressionUnit,
156
+ CustomHistoryPolicy,
157
+ CustomPrefixPolicy,
158
+ CustomTailTrimPolicy,
159
+ CustomCompressionPolicyV1,
160
+ CustomCompressionPolicyV2,
161
+ CustomCompressionPolicyV3,
162
+ ContextCompressionSettings,
163
+ HistoryMode,
164
+ PrunedEntry,
165
+ PruneResult,
166
+ PruneSessionOptions,
167
+ PruneStage,
168
+ ResolvedConfig,
169
+ ToolResultPruneConfig,
170
+ } from './runtime/types.ts'
171
+
172
+ // Re-export the canonical profile list from the type module as a runtime value.
173
+ export { COMPRESSION_PROFILES } from './runtime/types.ts'
174
+
175
+
176
+
177
+ declare module '@deepseek-ai/cordis' {
178
+ interface Context {
179
+ toolResultPruner: ToolResultPruner
180
+ }
181
+ }
182
+
183
+ /** Mixed deterministic selector behind the existing `ctx.toolResultPruner` seam. */
184
+ export class ToolResultPruner extends Service {
185
+ static inject = ['tokenMeter']
186
+
187
+ static Config: z<ToolResultPruneConfig> = z.object({
188
+ profile: z.union([...COMPRESSION_PROFILES]).default(DEFAULTS.profile),
189
+ headChars: z.number().step(1).min(0).default(DEFAULTS.headChars),
190
+ tailChars: z.number().step(1).min(0).default(DEFAULTS.tailChars),
191
+ nativeTriggerTokens: z.number().step(1).min(1).required(false),
192
+ nativeTargetTokens: z.number().step(1).min(1).required(false),
193
+ freshTriggerTokens: z.number().step(1).min(1).required(false),
194
+ freshTargetTokens: z.number().step(1).min(1).required(false),
195
+ aggregateTriggerTokens: z.number().step(1).min(1).required(false),
196
+ aggregateTargetTokens: z.number().step(1).min(1).required(false),
197
+ historyTriggerTokens: z.number().step(1).min(1).required(false),
198
+ historyKeepRecentToolCalls: z.number().step(1).min(0).required(false),
199
+ historyKeepRecentTokens: z.number().step(1).min(0).required(false),
200
+ historyMinReclaimTokens: z.number().step(1).min(1).required(false),
201
+ autoCompactThresholdPercent: z.number().step(1).min(50).max(90).required(false),
202
+ })
203
+
204
+ /** Consolidated per-session mutable state. */
205
+ readonly state: PrunerState
206
+
207
+ constructor(ctx: Context, config: ToolResultPruneConfig = {}) {
208
+ super(ctx, 'toolResultPruner')
209
+ ctx.inject(['tools', 'systemPrompt'], recoveryCtx => {
210
+ installContextCompressionRetrieve(recoveryCtx)
211
+ })
212
+ this.state = {
213
+ config: resolveConfig(config),
214
+ sessionSettings: new WeakMap(),
215
+ firstExposure: new WeakMap(),
216
+ recoveryExemptions: new WeakMap(),
217
+ dedupeTables: new WeakMap(),
218
+ estimatorVerdicts: new WeakMap(),
219
+ estimatorFailures: new WeakMap(),
220
+ warnedFailures: new WeakMap(),
221
+ postflightDiagnostics: new WeakMap(),
222
+ activeRequestBoundaries: new WeakMap(),
223
+ tailTrimBoundaryAttempts: new WeakMap(),
224
+ policyResolutionAudits: new WeakMap(),
225
+ }
226
+
227
+ ctx.on('session/event', (session, event) => {
228
+ if (event.type === 'compaction/summary') {
229
+ emitCompressionAudit(ctx.logger, {
230
+ schemaVersion: 1,
231
+ kind: 'native-auto-compact',
232
+ sessionId: String(session.id),
233
+ manifestEventType: 'compaction/summary',
234
+ manifestSeq: event.seq,
235
+ reducer: 'llm-summary',
236
+ provider: event.data.provider,
237
+ model: event.data.model,
238
+ tokensBefore: event.data.shadowedTokenCount,
239
+ tokensAfter: null,
240
+ })
241
+ return
242
+ }
243
+ if (event.type === 'compaction/end') {
244
+ // TokenPilot-inspired A2: annotate the landed summary checkpoint with
245
+ // an Exact Sources locator block. Strictly after compaction/end so no
246
+ // open-compaction invariant ever observes the rewrite.
247
+ try {
248
+ this.attachSummaryLocator(session, event.data.compactionId)
249
+ } catch (error: unknown) {
250
+ this.auditFailure(session, 'pressure', 'summary-locator', error)
251
+ ctx.logger.warn('context-compression summary locator failed open: %o', error)
252
+ }
253
+ }
254
+ })
255
+
256
+ // This is the true first-exposure boundary available in the Harness:
257
+ // the preceding step's results are already durable, the new step is open,
258
+ // and the next model request has not yet derived its history.
259
+ ctx.on('agent/pre-step', async ({ agent, signal, turn, step }, next) => {
260
+ const boundary = {}
261
+ this.state.activeRequestBoundaries.set(agent.session, boundary)
262
+ try {
263
+ if (!signal.aborted) {
264
+ try {
265
+ // Only the immediately preceding step can contain results that have
266
+ // not yet been exposed. This freezes both REDUCE and KEEP decisions:
267
+ // older original events are never reconsidered after a profile change.
268
+ this.runRequestBoundary(agent.session, turn, step - 1, signal)
269
+ } catch (error: unknown) {
270
+ this.auditFailure(agent.session, 'fresh', 'request-boundary', error)
271
+ ctx.logger.warn('context-compression fresh pass failed open: %o', error)
272
+ }
273
+ }
274
+ return await next()
275
+ } finally {
276
+ if (this.state.activeRequestBoundaries.get(agent.session) === boundary) {
277
+ this.state.activeRequestBoundaries.delete(agent.session)
278
+ }
279
+ }
280
+ }, { prepend: true })
281
+
282
+ ctx.on('agent/turn-stopping', ({ agent, turn, signal }) => {
283
+ if (signal.aborted) return
284
+ try {
285
+ const step = latestCompletedToolStep(agent.session, turn)
286
+ if (step !== undefined) this.runRequestBoundary(agent.session, turn, step, signal)
287
+ } catch (error: unknown) {
288
+ this.auditFailure(agent.session, 'fresh', 'terminal-pass', error)
289
+ ctx.logger.warn('context-compression terminal pass failed open: %o', error)
290
+ }
291
+ // TokenPilot-inspired E1: advisory estimator pass, strictly off the
292
+ // synchronous chain. Verdicts only feed the next pressure pass.
293
+ void this.postflightEstimatorPass(agent.session, signal).catch(() => undefined)
294
+ })
295
+ }
296
+
297
+ /**
298
+ * Measure text content in Unicode code points; non-text blocks cost zero.
299
+ * @param blocks - tool-result content to measure.
300
+ * @returns total Unicode code points across text blocks.
301
+ */
302
+ measureContent(blocks: readonly ContentBlock[]): number {
303
+ return measureContent(blocks)
304
+ }
305
+
306
+ /**
307
+ * Apply the configured native head/middle/tail transform.
308
+ * @param blocks - original tool-result content.
309
+ * @returns reduced content, or `null` when no reduction is required.
310
+ */
311
+ pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null {
312
+ return nativePruneContent(
313
+ blocks,
314
+ this.state.config.headChars + codePointLength(PRUNE_MARKER) + this.state.config.tailChars,
315
+ this.state.config.headChars,
316
+ this.state.config.tailChars,
317
+ )
318
+ }
319
+
320
+ /**
321
+ * Run one stable-surface pass. `fresh` is invoked before every request and
322
+ * only reduces original oversized results. `pressure` is called by
323
+ * compaction-basic and may additionally age old results at one high-water.
324
+ * @param session - session whose current tool-result surface may be rewritten.
325
+ * @param options - pass stage and optional completed-step coordinates.
326
+ * @returns landed replacements and aggregate Unicode-code-point savings.
327
+ */
328
+ pruneSession(session: Session, options: PruneSessionOptions = {}): PruneResult {
329
+ const stage = options.stage ?? 'pressure'
330
+ // External callers (compaction-basic) do not carry routed capacity; the
331
+ // runtime resolves it itself so the frozen Auto Compact linkage and the
332
+ // Custom percentage policy see one consistent context window.
333
+ const contextWindowTokens = options.contextWindowTokens ?? this.contextWindowForRequest(session)
334
+ const policy = this.activePolicy(session, contextWindowTokens, stage)
335
+ if (policy === undefined) return emptyResult()
336
+ const profile = policy.profile
337
+ const view = measureForCompaction(this.ctx, session)
338
+ if (stage === 'fresh') return this.decideFreshStep(session, options, policy, view)
339
+ if (profile === 'off') return emptyResult()
340
+
341
+ const landed: PrunedEntry[] = []
342
+ if (policy.nativeToolResultEnabled) {
343
+ const candidates = this.snapshot(session, view)
344
+ const eligible = candidates.filter(candidate => !this.isRecoveryExempt(session, candidate))
345
+ const exactUnavailable = eligible.some(candidate => candidate.count.kind !== 'exact-tokenizer')
346
+ if (exactUnavailable) {
347
+ this.warnExactUnavailable(session, view, 'native')
348
+ }
349
+ const planned = eligible
350
+ .map(candidate => this.planNative(candidate, session, stage, policy, view))
351
+ .filter((entry): entry is PlannedReplacement => entry !== null)
352
+ landed.push(...this.landAll(session, planned))
353
+ if (landed.length === 0) {
354
+ const exact = eligible.flatMap(candidate => candidate.count.kind === 'exact-tokenizer'
355
+ ? [candidate.count.tokens] : [])
356
+ this.auditComponent(session, policy, 'native-tool-result', 'pressure', 'skipped',
357
+ exactUnavailable ? 'exact-tokenizer-unavailable'
358
+ : exact.length === 0 ? 'no-tool-result-candidates'
359
+ : Math.max(...exact) <= policy.nativeTriggerTokens ? 'at-or-below-trigger'
360
+ : planned.length === 0 ? 'no-valid-reduction'
361
+ : 'recovery-tool-unavailable', {
362
+ measurementKind: exactUnavailable ? 'unavailable' : 'exact-tokenizer',
363
+ ...(exact.length === 0 ? {} : { currentTokens: Math.max(...exact) }),
364
+ triggerTokens: policy.nativeTriggerTokens,
365
+ targetTokens: policy.nativeTargetTokens,
366
+ })
367
+ }
368
+ return summarize(landed)
369
+ }
370
+
371
+ let historyOutcome: HistoryPlanOutcome = { kind: 'planned', plans: [] }
372
+ let historyAllowed = false
373
+ if (policy.historyMode === 'adaptive') {
374
+ historyOutcome = this.planHistoricalAging(session, policy, view)
375
+ // Adaptive cost authority is meaningful only after the structural
376
+ // planner has formed a real batch. A planning skip keeps its own reason
377
+ // (for example exact-tokenizer-unavailable) and never becomes a false
378
+ // adaptive-cost-rejected decision merely because it has zero plans.
379
+ if (historyOutcome.kind === 'planned') {
380
+ const capacityPressure = this.capacityPressureActive(session, view, policy)
381
+ historyAllowed = this.adaptiveHistoryAllowed(
382
+ session,
383
+ view,
384
+ historyOutcome.plans,
385
+ capacityPressure,
386
+ )
387
+ if (historyAllowed) {
388
+ landed.push(...this.landAll(session, historyOutcome.plans))
389
+ }
390
+ }
391
+ } else {
392
+ historyAllowed = this.historyAllowed(session, policy, view)
393
+ if (historyAllowed) {
394
+ historyOutcome = this.planHistoricalAging(session, policy, view)
395
+ if (historyOutcome.kind === 'planned') {
396
+ landed.push(...this.landAll(session, historyOutcome.plans))
397
+ }
398
+ }
399
+ }
400
+ if (!landed.some(entry => entry.stage === 'pressure')) {
401
+ this.auditHistoryEvaluation(session, policy, view, historyAllowed, historyOutcome)
402
+ }
403
+ if (policy.tailTrim?.enabled === true) {
404
+ const tailView = measureForCompaction(this.ctx, session)
405
+ this.landOldestTailTrimGroup(session, policy, tailView)
406
+ } else {
407
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'disabled', 'profile-policy')
408
+ }
409
+ return summarize(landed)
410
+ }
411
+
412
+ private activeSettings(session: Session): ContextCompressionSettings {
413
+ const frozen = this.state.sessionSettings.get(session)
414
+ if (frozen !== undefined) return frozen
415
+ // Harness 0.1.1 brands namespace values through a helper while 0.1.2
416
+ // validates the same public literal at its SettingsProvider boundary.
417
+ const settings = this.ctx.get('settings')?.get(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as never)
418
+ let resolved: ContextCompressionSettings
419
+ let settingsSource: 'host-settings' | 'plugin-config-fallback' = settings === undefined
420
+ ? 'plugin-config-fallback'
421
+ : 'host-settings'
422
+ let autoCompactThresholdSource: 'generation-config' | 'host-settings' | 'schema-default'
423
+ = settings === undefined ? 'schema-default' : 'host-settings'
424
+ let settingsInvalidFallback: 'lossless-off' | undefined
425
+ try {
426
+ resolved = settings === undefined
427
+ ? ContextCompressionSettingsSchema({ profile: this.state.config.profile } as never)
428
+ // Validate the Host value BEFORE cloning: structuredClone normalizes
429
+ // class/exotic prototypes to Object.prototype and would otherwise
430
+ // erase the very boundary the parser is responsible for enforcing.
431
+ : parseContextCompressionSettings(settings)
432
+ } catch (error: unknown) {
433
+ // A malformed persisted document must fail open LOSSLESSLY: freezing the
434
+ // deployment default could silently re-enable lossy compression the
435
+ // user never chose (for example a stored `off` plus an unknown key), so
436
+ // the session freezes effectively off and keeps every original result.
437
+ const reason = error instanceof Error ? error.message : String(error)
438
+ this.auditFailure(session, 'pressure', 'policy-resolution', error)
439
+ this.warnOnce(
440
+ session,
441
+ `settings-invalid:${reason}`,
442
+ 'context-compression froze this session effectively off because the stored settings document is invalid: %s',
443
+ reason,
444
+ )
445
+ resolved = ContextCompressionSettingsSchema({ profile: 'off' } as never)
446
+ settingsSource = 'plugin-config-fallback'
447
+ autoCompactThresholdSource = 'schema-default'
448
+ settingsInvalidFallback = 'lossless-off'
449
+ }
450
+ if (this.state.config.autoCompactThresholdPercent !== undefined) {
451
+ // The preset overlay froze this generation's threshold into the
452
+ // deployment config; it supersedes the live Host setting so Auto
453
+ // Compact and micro compact can never split across two thresholds.
454
+ resolved = {
455
+ ...resolved,
456
+ autoCompact: { thresholdPercent: this.state.config.autoCompactThresholdPercent },
457
+ }
458
+ autoCompactThresholdSource = 'generation-config'
459
+ }
460
+ const snapshot = deepFreeze(structuredClone(resolved))
461
+ this.state.sessionSettings.set(session, snapshot)
462
+ emitCompressionAudit(this.ctx.logger, {
463
+ schemaVersion: 1,
464
+ kind: 'policy-frozen',
465
+ sessionId: String(session.id),
466
+ settingsSource,
467
+ autoCompactThresholdSource,
468
+ ...settingsInvalidFallback === undefined ? {} : { settingsInvalidFallback },
469
+ settings: snapshot,
470
+ deploymentConfig: this.state.config,
471
+ })
472
+ return snapshot
473
+ }
474
+
475
+ /**
476
+ * TokenPilot-inspired A2: replace the compaction summary checkpoint node
477
+ * with the same summary plus an Exact Sources locator block. Fails open:
478
+ * any unresolved shape (no trace, no checkpoint node, already annotated)
479
+ * leaves the summary untouched.
480
+ */
481
+ private attachSummaryLocator(session: Session, compactionId: string): void {
482
+ const policy = this.activePolicy(session)
483
+ if (policy?.presetOptions?.summaryLocator !== true) return
484
+ const events = sessionEvents(session)
485
+ const trace = findCompactionTrace(events, compactionId)
486
+ if (trace === undefined) return
487
+ const located = buildLocatorBlock(events, trace.summaryShadowedRange)
488
+ if (located === null) return
489
+ const block = located.text
490
+ // Locate the summary checkpoint surface node: the user/message replacement
491
+ // carrying this compaction's checkpoint provenance. Newest match wins.
492
+ let checkpointSeq: number | undefined
493
+ for (const seq of [...session.surface.nodes].reverse()) {
494
+ const event = events[seq]
495
+ if (event === undefined || event.type !== 'user/message') continue
496
+ const source = (event.data as { source?: { compactionId?: unknown } }).source
497
+ if (source === undefined || source === null) continue
498
+ if (source.compactionId !== compactionId) continue
499
+ checkpointSeq = seq
500
+ break
501
+ }
502
+ if (checkpointSeq === undefined) return
503
+ const original = events[checkpointSeq]
504
+ if (original?.type !== 'user/message') return
505
+ const data = original.data as UserMessage & { source?: unknown }
506
+ const content = data.content.map(block => ({ ...block })) as typeof data.content
507
+ const textBlocks = content.filter((block): block is Extract<(typeof content)[number], { type: 'text' }> => block.type === 'text')
508
+ const lastText = textBlocks.at(-1)
509
+ const marker = '## Exact Sources (locators)'
510
+ if (lastText === undefined) return
511
+ if (lastText.text.includes(marker)) return
512
+ lastText.text = `${lastText.text}\n\n${block}`
513
+ // Drop the checkpoint provenance: this replacement is a plain plugin-source
514
+ // user/message surface rewrite, not a new compaction checkpoint, and
515
+ // carrying the marker would fail the host's closed-transaction validation.
516
+ const replacement = createUserMessage({
517
+ content,
518
+ source: { kind: 'plugin', plugin: 'dsh-context-compression-improved-runtime' },
519
+ })
520
+ session.append('user/message', replacement, {
521
+ surfaceOp: { op: 'replace', start: checkpointSeq, end: checkpointSeq },
522
+ sourceEventSeqs: [checkpointSeq],
523
+ })
524
+ emitCompressionAudit(this.ctx.logger, {
525
+ schemaVersion: 1,
526
+ kind: 'summary-locator',
527
+ sessionId: String(session.id),
528
+ profile: policy.profile,
529
+ checkpointSeq,
530
+ summarySeq: trace.summarySeq,
531
+ locatorChars: codePointLength(located.text),
532
+ spillFiles: located.spillFiles,
533
+ touchedFiles: located.touchedFiles,
534
+ })
535
+ }
536
+
537
+ /**
538
+ * TokenPilot-inspired E1: sample oversized historical reads and ask the
539
+ * auxiliary estimator whether their file state is still likely to be
540
+ * referenced. Fire-and-forget: never awaited on the pruning chain, failures
541
+ * back off exponentially per Session, verdicts only extend the rule-only
542
+ * superseded classification.
543
+ */
544
+ private async postflightEstimatorPass(session: Session, signal: AbortSignal): Promise<void> {
545
+ const policy = this.activePolicy(session)
546
+ const presetOptions = policy?.presetOptions
547
+ if (policy === undefined || presetOptions?.readState !== true) return
548
+ const estimatorMode = presetOptions.estimator?.mode ?? ''
549
+ if (estimatorMode === '') return
550
+ const failures = this.state.estimatorFailures.get(session)
551
+ if (isCoolingDown(failures, Date.now())) return
552
+
553
+ const events = sessionEvents(session)
554
+ const samples: EstimatorSample[] = []
555
+ const now = Date.now()
556
+ for (const candidate of this.snapshot(session, measureForCompaction(this.ctx, session))) {
557
+ if (samples.length >= 3) break
558
+ if (candidate.event.data.turn === undefined) continue
559
+ const tokens = exactTokens(candidate.count)
560
+ if (tokens === undefined || tokens <= policy.freshTriggerTokens) continue
561
+ const path = toolCallPath(candidate.call.arguments)
562
+ if (path === undefined) continue
563
+ if (isSupersededRead(events, candidate.seq, path)) continue
564
+ if (this.state.estimatorVerdicts.get(session)?.has(candidate.seq) === true) continue
565
+ samples.push({ seq: candidate.seq, path, turn: candidate.event.data.turn })
566
+ }
567
+ if (samples.length === 0) return
568
+
569
+ const estimator = new Estimator(this.ctx, this.activeSettings(session).presetOptions ?? {})
570
+ const answer = await estimator.ask(buildEstimatorSystemPrompt(), buildEstimatorUserPrompt(samples), signal)
571
+ const latencyMs = Date.now() - now
572
+ const ok = answer !== undefined && signal.aborted === false
573
+ let expired = 0
574
+ if (ok && answer !== undefined) {
575
+ let verdicts = this.state.estimatorVerdicts.get(session)
576
+ if (verdicts === undefined) {
577
+ verdicts = new Map()
578
+ this.state.estimatorVerdicts.set(session, verdicts)
579
+ }
580
+ for (const verdict of parseEstimatorAnswer(answer)) {
581
+ if (verdicts.has(verdict.seq)) continue
582
+ verdicts.set(verdict.seq, verdict.expired)
583
+ if (verdict.expired) expired += 1
584
+ }
585
+ } else {
586
+ const next: EstimatorFailures = {
587
+ failures: (failures?.failures ?? 0) + 1,
588
+ cooldownUntil: Date.now() + backoffCooldownMs((failures?.failures ?? 0) + 1),
589
+ }
590
+ this.state.estimatorFailures.set(session, next)
591
+ }
592
+ emitCompressionAudit(this.ctx.logger, {
593
+ schemaVersion: 1,
594
+ kind: 'estimator-outcome',
595
+ sessionId: String(session.id),
596
+ profile: policy.profile,
597
+ channel: estimatorMode === 'host' ? 'host' : 'direct',
598
+ sampled: samples.length,
599
+ expired,
600
+ latencyMs,
601
+ ok,
602
+ })
603
+ }
604
+
605
+ private activePolicy(
606
+ session: Session,
607
+ contextWindowTokens?: number,
608
+ stage: PruneStage = 'pressure',
609
+ ): CompressionPolicy | undefined {
610
+ const settings = this.activeSettings(session)
611
+ try {
612
+ const policy = resolvePolicy(
613
+ this.state.config,
614
+ settings.profile,
615
+ settings.custom,
616
+ {
617
+ ...contextWindowTokens === undefined ? {} : { contextWindowTokens },
618
+ autoCompactThresholdPercent: settings.autoCompact.thresholdPercent,
619
+ },
620
+ )
621
+ // Route changes must produce a fresh audit record even when the policy
622
+ // object is unchanged, or the dedupe would hide a mid-session reroute.
623
+ const route = routeAuditFact(session)
624
+ const auditKey = JSON.stringify({
625
+ policy,
626
+ contextWindowTokens: contextWindowTokens ?? null,
627
+ route: route ?? null,
628
+ })
629
+ // Deduplicate only CONSECUTIVE identical resolutions: a permanent set
630
+ // would hide an A -> B -> A reroute's third record.
631
+ if (this.state.policyResolutionAudits.get(session) !== auditKey) {
632
+ this.state.policyResolutionAudits.set(session, auditKey)
633
+ // Deployment config overrides win over the Auto Compact linkage, so a
634
+ // standard profile whose History watermarks were replaced must not
635
+ // audit itself as purely linkage-derived.
636
+ const overriddenLinkedFields = ([
637
+ 'historyTriggerTokens',
638
+ 'historyKeepRecentTokens',
639
+ 'historyMinReclaimTokens',
640
+ ] as const).filter(key => this.state.config[key] !== undefined).length
641
+ emitCompressionAudit(this.ctx.logger, {
642
+ schemaVersion: 1,
643
+ kind: 'policy-resolved',
644
+ sessionId: String(session.id),
645
+ policy,
646
+ ...contextWindowTokens === undefined ? {} : { contextWindowTokens },
647
+ coordination: {
648
+ thresholdPercent: settings.autoCompact.thresholdPercent,
649
+ ...policy.autoCompactTokens === undefined ? {} : { autoCompactTokens: policy.autoCompactTokens },
650
+ ...policy.microDeadlineTokens === undefined ? {} : { microDeadlineTokens: policy.microDeadlineTokens },
651
+ paramSource: settings.profile === 'custom'
652
+ ? 'custom-manual'
653
+ : overriddenLinkedFields === 3 ? 'deployment-override'
654
+ : overriddenLinkedFields > 0 ? 'mixed'
655
+ : policy.microDeadlineTokens === undefined ? 'fixed-preset' : 'auto-compact-linked',
656
+ },
657
+ ...route === undefined ? {} : { route },
658
+ ...route === undefined ? {} : tokenizerAuditFact(route),
659
+ })
660
+ }
661
+ return policy
662
+ } catch (error: unknown) {
663
+ const reason = error instanceof Error ? error.message : String(error)
664
+ this.auditFailure(session, stage, 'policy-resolution', error)
665
+ this.warnOnce(
666
+ session,
667
+ `custom-policy:${settings.profile}:${reason}`,
668
+ 'context-compression kept original tool results because the Custom policy is not effective: %s',
669
+ reason,
670
+ )
671
+ return undefined
672
+ }
673
+ }
674
+
675
+
676
+
677
+ private contextWindowForRequest(
678
+ session: Session,
679
+ ): number | undefined {
680
+ const settings = this.activeSettings(session)
681
+ // History linkage needs routed capacity for standard profiles, and the
682
+ // Custom percentage policy needs it for context-percent documents. Off and
683
+ // Native never link, and token-unit Custom stays manual.
684
+ if (settings.profile === 'off' || settings.profile === 'native') return undefined
685
+ if (settings.profile === 'custom' && settings.custom.unit !== 'context-percent') return undefined
686
+ const config = session.requestHeader()?.config
687
+ const routed = session.requestContext()
688
+ if (config === undefined || config.provider.length === 0 || config.model.length === 0 || routed === undefined) {
689
+ return undefined
690
+ }
691
+ if (routed.provider !== config.provider || routed.model !== config.model) {
692
+ this.warnOnce(
693
+ session,
694
+ `custom-context-window-route:${config.provider}\0${config.model}`,
695
+ 'context-compression kept the context-linked policy inactive because durable route capacity belongs to %s/%s, not %s/%s',
696
+ routed.provider,
697
+ routed.model,
698
+ config.provider,
699
+ config.model,
700
+ )
701
+ return undefined
702
+ }
703
+ if (!Number.isSafeInteger(routed.contextWindow) || routed.contextWindow === undefined || routed.contextWindow <= 0) {
704
+ this.warnOnce(
705
+ session,
706
+ `custom-context-window-capacity:${config.provider}\0${config.model}`,
707
+ 'context-compression kept the context-linked policy inactive because %s/%s has no positive durable context capacity',
708
+ config.provider,
709
+ config.model,
710
+ )
711
+ return undefined
712
+ }
713
+ return routed.contextWindow
714
+ }
715
+
716
+ private runRequestBoundary(
717
+ session: Session,
718
+ turn: number,
719
+ step: number,
720
+ signal: AbortSignal,
721
+ ): void {
722
+ const contextWindowTokens = this.contextWindowForRequest(session)
723
+ if (signal.aborted) return
724
+ const policy = this.activePolicy(session, contextWindowTokens, 'fresh')
725
+ if (policy === undefined) return
726
+ const capacity = contextWindowTokens === undefined ? {} : { contextWindowTokens }
727
+ this.pruneSession(session, { stage: 'fresh', freshTurn: turn, freshStep: step, ...capacity })
728
+ if (policy.historyMode !== 'disabled' || policy.tailTrim?.enabled === true) {
729
+ this.pruneSession(session, { stage: 'pressure', ...capacity })
730
+ }
731
+ }
732
+
733
+ /** Resolve historical-aging authority without accepting caller-supplied elevation. */
734
+ private historyAllowed(session: Session, policy: CompressionPolicy, view: CompactionTokenView): boolean {
735
+ switch (policy.historyMode) {
736
+ case 'disabled':
737
+ return false
738
+ case 'routine':
739
+ return true
740
+ case 'capacity-pressure':
741
+ return this.capacityPressureActive(session, view, policy)
742
+ case 'adaptive':
743
+ return false
744
+ /* v8 ignore next -- closed-union exhaustiveness guard */
745
+ default:
746
+ return assertNever(policy.historyMode, 'history mode')
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Match the compaction-basic pressure gate using public durable data. The
752
+ * frozen Auto Compact deadline `D = floor(A x 0.875)` replaces the legacy
753
+ * fixed 0.7 ratio once the standard-profile linkage resolved; without
754
+ * linkage the 0.7 ratio is the documented fallback and reproduces the
755
+ * previous behavior.
756
+ */
757
+ private capacityPressureActive(
758
+ session: Session,
759
+ view: CompactionTokenView,
760
+ policy: CompressionPolicy,
761
+ ): boolean {
762
+ const deadline = policy.microDeadlineTokens
763
+ if (deadline !== undefined) return view.totalTokens >= deadline
764
+ const header = session.requestHeader()?.config
765
+ const routed = session.requestContext()
766
+ const contextWindow = routed?.contextWindow
767
+ if (header === undefined || routed === undefined
768
+ || routed.provider !== header.provider
769
+ || routed.model !== header.model
770
+ || contextWindow === undefined
771
+ || !Number.isSafeInteger(contextWindow)
772
+ || contextWindow <= 0) return false
773
+ return view.totalTokens >= Math.floor(contextWindow * CAPACITY_PRESSURE_RATIO)
774
+ }
775
+
776
+ /** Emit one bounded, independently correlatable postflight cost diagnostic per completed attempt. */
777
+ private logAdaptivePostflight(session: Session, usage: ObservedPromptUsage): void {
778
+ const attemptId = String(usage.attemptId)
779
+ if (this.state.postflightDiagnostics.get(session) === attemptId) return
780
+ this.state.postflightDiagnostics.set(session, attemptId)
781
+
782
+ const key = usage.key
783
+ let priceRecord: Readonly<Record<string, unknown>> | undefined
784
+ let cost: ReturnType<typeof priceOfficialDeepSeekUsage> | { readonly kind: 'unpriced'; readonly reason: string }
785
+ if (key === undefined) {
786
+ cost = { kind: 'unpriced', reason: 'measurement key unavailable' }
787
+ } else if (usage.responseModelId !== key.modelId) {
788
+ cost = { kind: 'unpriced', reason: 'response model mismatch or unavailable' }
789
+ } else if (usage.observedOutputTokens === undefined) {
790
+ cost = { kind: 'unpriced', reason: 'output token count unavailable' }
791
+ } else if (usage.cacheStatus !== 'complete'
792
+ || usage.cacheReadTokens === undefined
793
+ || usage.cacheMissTokens === undefined) {
794
+ cost = { kind: 'unpriced', reason: 'complete cache split unavailable' }
795
+ } else {
796
+ const startedAt = new Date(usage.startedAtMs)
797
+ const completedAt = new Date(usage.completedAtMs)
798
+ const resolution = resolveOfficialDeepSeekPrice({
799
+ provider: key.provider,
800
+ baseUrlClass: key.baseUrlClass,
801
+ apiRoute: key.apiRoute,
802
+ modelId: key.modelId,
803
+ currency: 'USD',
804
+ at: startedAt,
805
+ })
806
+ if (resolution.kind === 'priced') {
807
+ priceRecord = {
808
+ catalogVersion: resolution.record.catalogVersion,
809
+ checkedAt: resolution.record.checkedAt,
810
+ sourceUrl: resolution.record.sourceUrl,
811
+ currency: resolution.record.currency,
812
+ modelId: resolution.record.modelId,
813
+ apiRoute: resolution.record.apiRoute,
814
+ startBand: resolution.record.band,
815
+ }
816
+ }
817
+ cost = priceOfficialDeepSeekUsage({
818
+ provider: key.provider,
819
+ baseUrlClass: key.baseUrlClass,
820
+ apiRoute: key.apiRoute,
821
+ modelId: key.modelId,
822
+ currency: 'USD',
823
+ startedAt,
824
+ completedAt,
825
+ usage: {
826
+ cacheReadTokens: usage.cacheReadTokens,
827
+ cacheMissTokens: usage.cacheMissTokens,
828
+ outputTokens: usage.observedOutputTokens,
829
+ },
830
+ })
831
+ }
832
+
833
+ this.ctx.logger.debug(`context-compression adaptive postflight ${JSON.stringify({
834
+ sessionId: String(session.id),
835
+ providerRequestOrdinal: Number(usage.providerRequestOrdinal),
836
+ attemptId,
837
+ startedAtMs: usage.startedAtMs,
838
+ completedAtMs: usage.completedAtMs,
839
+ measurementKind: usage.measurement.kind,
840
+ catalogVersion: DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION,
841
+ ...(priceRecord === undefined ? {} : { priceRecord }),
842
+ usage: {
843
+ promptTokens: usage.observedPromptTokens,
844
+ ...(usage.observedOutputTokens === undefined ? {} : { outputTokens: usage.observedOutputTokens }),
845
+ cacheStatus: usage.cacheStatus ?? 'unknown',
846
+ ...(usage.cacheReadTokens === undefined ? {} : { cacheReadTokens: usage.cacheReadTokens }),
847
+ ...(usage.cacheMissTokens === undefined ? {} : { cacheMissTokens: usage.cacheMissTokens }),
848
+ },
849
+ cost,
850
+ })}`)
851
+ }
852
+
853
+ /** Decide one already-planned History batch from adjacent request-level facts only. */
854
+ private adaptiveHistoryAllowed(
855
+ session: Session,
856
+ view: CompactionTokenView,
857
+ plans: readonly PlannedReplacement[],
858
+ capacityPressure: boolean,
859
+ ): boolean {
860
+ const log = (
861
+ allowHistory: boolean,
862
+ reason: string,
863
+ detail: Readonly<Record<string, unknown>> = {},
864
+ ): boolean => {
865
+ this.ctx.logger.debug(`context-compression adaptive ${JSON.stringify({
866
+ sessionId: String(session.id),
867
+ allowHistory,
868
+ reason,
869
+ catalogVersion: DEEPSEEK_OFFICIAL_PRICE_CATALOG_VERSION,
870
+ ...detail,
871
+ })}`)
872
+ return allowHistory
873
+ }
874
+ const usage = view.lastCompletedUsage
875
+ if (usage !== undefined) this.logAdaptivePostflight(session, usage)
876
+ if (plans.length === 0) return false
877
+ if (capacityPressure) return log(true, 'capacity-override')
878
+
879
+ const currentKey = view.latestEnvelopeKey
880
+ if (usage === undefined) return log(false, 'usage-unavailable')
881
+ if (usage.key === undefined || currentKey === undefined) {
882
+ return log(false, 'measurement-key-unavailable')
883
+ }
884
+ if (!sameProviderMeasurementKey(usage.key, currentKey)) {
885
+ return log(false, 'measurement-key-mismatch')
886
+ }
887
+ if (usage.responseModelId !== usage.key.modelId) {
888
+ return log(false, 'response-model-mismatch-or-unavailable')
889
+ }
890
+ if (usage.cacheStatus !== 'complete'
891
+ || usage.cacheReadTokens === undefined
892
+ || usage.cacheMissTokens === undefined) {
893
+ return log(false, 'cache-split-incomplete')
894
+ }
895
+
896
+ const price = resolveOfficialDeepSeekPrice({
897
+ provider: usage.key.provider,
898
+ baseUrlClass: usage.key.baseUrlClass,
899
+ apiRoute: usage.key.apiRoute,
900
+ modelId: usage.key.modelId,
901
+ currency: 'USD',
902
+ at: new Date(),
903
+ })
904
+ if (price.kind === 'unpriced') return log(false, `adaptive-unknown-price:${price.reason}`)
905
+
906
+ const exactReclaimedTokens = plans.reduce(
907
+ (sum, plan) => sum + plan.tokensBefore - plan.tokensAfter,
908
+ 0,
909
+ )
910
+ const earliestChangedSeq = Math.min(...plans.map(plan => plan.candidate.seq))
911
+ const bounds = deriveAdaptiveTokenBounds({
912
+ exactReclaimedTokens,
913
+ earliestChangedSeq,
914
+ previousPromptTokens: usage.observedPromptTokens,
915
+ expectedTokenizerRevision: usage.key.tokenizerRevision,
916
+ previousRequestMeasurement: usage.measurement,
917
+ measuredNodes: view.measuredNodes,
918
+ })
919
+ const decision = decideConservativeAdaptive({
920
+ capacityPressure: false,
921
+ bounds,
922
+ inputCacheHitRate: price.record.inputCacheHit,
923
+ inputCacheMissRate: price.record.inputCacheMiss,
924
+ observedCacheReadTokens: usage.cacheReadTokens,
925
+ })
926
+ return log(decision.allowHistory, decision.reason, {
927
+ priceBand: price.record.band,
928
+ observedPromptTokens: usage.observedPromptTokens,
929
+ observedCacheReadTokens: usage.cacheReadTokens,
930
+ bounds,
931
+ ...'minimumRemovalValue' in decision
932
+ ? { minimumRemovalValue: decision.minimumRemovalValue }
933
+ : {},
934
+ ...'maximumCacheLossPenalty' in decision
935
+ ? { maximumCacheLossPenalty: decision.maximumCacheLossPenalty }
936
+ : {},
937
+ })
938
+ }
939
+
940
+ private decisions(session: Session): Set<number> {
941
+ let decisions = this.state.firstExposure.get(session)
942
+ if (decisions === undefined) {
943
+ decisions = new Set()
944
+ this.state.firstExposure.set(session, decisions)
945
+ }
946
+ return decisions
947
+ }
948
+
949
+ /**
950
+ * TokenPilot-style skipReduction: recovery tool output is permanently exempt
951
+ * from every reduction pass so retrieved content can never enter a
952
+ * compress-restore-oscillation loop. A call-name match covers the built-in
953
+ * recovery tool; the per-session set admits future recovery paths.
954
+ */
955
+ private isRecoveryExempt(session: Session, candidate: SnapshotCandidate): boolean {
956
+ if (candidate.call.name === 'context_compression_retrieve') return true
957
+ return this.state.recoveryExemptions.get(session)?.has(candidate.seq) ?? false
958
+ }
959
+
960
+ /** Register a result seq as permanently exempt from further reduction. */
961
+ private grantRecoveryExemption(session: Session, seq: number): void {
962
+ let exemptions = this.state.recoveryExemptions.get(session)
963
+ if (exemptions === undefined) {
964
+ exemptions = new Set()
965
+ this.state.recoveryExemptions.set(session, exemptions)
966
+ }
967
+ exemptions.add(seq)
968
+ }
969
+
970
+ private decideFreshStep(
971
+ session: Session,
972
+ options: PruneSessionOptions,
973
+ policy: CompressionPolicy,
974
+ view: CompactionTokenView,
975
+ ): PruneResult {
976
+ if (options.freshTurn === undefined || options.freshStep === undefined) {
977
+ this.auditComponent(session, policy, 'fresh', 'fresh',
978
+ policy.freshEnabled ? 'skipped' : 'disabled',
979
+ policy.freshEnabled ? 'missing-completed-step-coordinates' : 'profile-policy')
980
+ this.auditComponent(session, policy, 'aggregate', 'fresh',
981
+ policy.aggregateEnabled ? 'skipped' : 'disabled',
982
+ policy.aggregateEnabled ? 'missing-completed-step-coordinates' : 'profile-policy')
983
+ return emptyResult()
984
+ }
985
+ const decisions = this.decisions(session)
986
+ const candidates = this.snapshot(session, view).filter(candidate =>
987
+ typeof candidate.event.surfaceOp !== 'object'
988
+ && candidate.event.data.turn === options.freshTurn
989
+ && candidate.event.data.step === options.freshStep
990
+ && !decisions.has(candidate.seq))
991
+ if (candidates.length === 0) {
992
+ this.auditComponent(session, policy, 'fresh', 'fresh',
993
+ policy.freshEnabled ? 'skipped' : 'disabled',
994
+ policy.freshEnabled ? 'no-new-tool-result-candidates' : 'profile-policy')
995
+ this.auditComponent(session, policy, 'aggregate', 'fresh',
996
+ policy.aggregateEnabled ? 'skipped' : 'disabled',
997
+ policy.aggregateEnabled ? 'no-new-tool-result-candidates' : 'profile-policy')
998
+ return emptyResult()
999
+ }
1000
+
1001
+ const plans = new Map<number, PlannedReplacement>()
1002
+ let freshPlanned = 0
1003
+ const dedupeEnabled = policy.presetOptions?.dedupeToolResults === true
1004
+ const exactCandidateTokens = candidates.map(candidate => exactTokens(candidate.count))
1005
+ const exactAvailable = exactCandidateTokens.every(tokens => tokens !== undefined)
1006
+ const maxCandidateTokens = exactAvailable
1007
+ ? Math.max(...exactCandidateTokens as number[])
1008
+ : undefined
1009
+ if (policy.freshEnabled) {
1010
+ if (candidates.some(candidate => candidate.call.name !== 'context_compression_retrieve'
1011
+ && candidate.count.kind !== 'exact-tokenizer')) {
1012
+ this.warnExactUnavailable(session, view, 'fresh')
1013
+ }
1014
+ for (const candidate of candidates) {
1015
+ if (this.isRecoveryExempt(session, candidate)) continue
1016
+ if (dedupeEnabled) {
1017
+ const dedupePlan = this.planDedupe(candidate, session, policy, view)
1018
+ if (dedupePlan !== null) {
1019
+ plans.set(candidate.seq, dedupePlan)
1020
+ continue
1021
+ }
1022
+ }
1023
+ const plan = this.planFresh(candidate, session, policy, view)
1024
+ if (plan !== null) {
1025
+ plans.set(candidate.seq, plan)
1026
+ freshPlanned += 1
1027
+ }
1028
+ }
1029
+ }
1030
+ let aggregateInputTokens: number | undefined
1031
+ let aggregatePlanned = 0
1032
+ if (policy.aggregateEnabled) {
1033
+ const aggregateAvailable = exactAvailable
1034
+ if (!aggregateAvailable) this.warnExactUnavailable(session, view, 'aggregate')
1035
+ let total = aggregateAvailable
1036
+ ? candidates.reduce((sum, candidate) => sum + (plans.get(candidate.seq)?.tokensAfter
1037
+ ?? exactTokens(candidate.count) ?? 0), 0)
1038
+ : 0
1039
+ if (aggregateAvailable) aggregateInputTokens = total
1040
+ if (aggregateAvailable && total > policy.aggregateTriggerTokens) {
1041
+ const remaining = candidates
1042
+ .filter(candidate => !this.isRecoveryExempt(session, candidate))
1043
+ .sort((a, b) => Number(isError(a)) - Number(isError(b))
1044
+ || (plans.get(b.seq)?.tokensAfter ?? exactTokens(b.count) ?? 0)
1045
+ - (plans.get(a.seq)?.tokensAfter ?? exactTokens(a.count) ?? 0))
1046
+ for (const candidate of remaining) {
1047
+ const previous = plans.get(candidate.seq)
1048
+ const plan = this.planAggregate(candidate, session, view)
1049
+ const previousTokens = previous?.tokensAfter ?? exactTokens(candidate.count) ?? 0
1050
+ if (plan === null || plan.tokensAfter >= previousTokens) continue
1051
+ plans.set(candidate.seq, plan)
1052
+ aggregatePlanned += 1
1053
+ total -= previousTokens - plan.tokensAfter
1054
+ if (total <= policy.aggregateTargetTokens) break
1055
+ }
1056
+ if (total > policy.aggregateTargetTokens) {
1057
+ this.ctx.logger.warn(
1058
+ 'context-compression fresh aggregate residual: %d tokens exceed target %d',
1059
+ total,
1060
+ policy.aggregateTargetTokens,
1061
+ )
1062
+ }
1063
+ }
1064
+ }
1065
+
1066
+ const landed = this.landAll(session, candidates
1067
+ .map(candidate => plans.get(candidate.seq))
1068
+ .filter((plan): plan is PlannedReplacement => plan !== undefined))
1069
+ const freshLanded = landed.some(entry => entry.stage === 'fresh'
1070
+ && plans.get(entry.originalSeq)?.component === 'fresh')
1071
+ const aggregateLanded = landed.some(entry => entry.stage === 'fresh'
1072
+ && plans.get(entry.originalSeq)?.component === 'aggregate')
1073
+ if (!freshLanded) {
1074
+ this.auditComponent(session, policy, 'fresh', 'fresh',
1075
+ policy.freshEnabled ? 'skipped' : 'disabled',
1076
+ !policy.freshEnabled ? 'profile-policy'
1077
+ : !exactAvailable ? 'exact-tokenizer-unavailable'
1078
+ : (maxCandidateTokens ?? 0) <= policy.freshTriggerTokens ? 'at-or-below-trigger'
1079
+ : freshPlanned > 0 && aggregatePlanned > 0 ? 'superseded-by-aggregate'
1080
+ : freshPlanned === 0 ? 'no-valid-reduction'
1081
+ : 'recovery-tool-unavailable', {
1082
+ measurementKind: exactAvailable ? 'exact-tokenizer' : 'unavailable',
1083
+ ...(maxCandidateTokens === undefined ? {} : { currentTokens: maxCandidateTokens }),
1084
+ triggerTokens: policy.freshTriggerTokens,
1085
+ targetTokens: policy.freshTargetTokens,
1086
+ })
1087
+ }
1088
+ if (!aggregateLanded) {
1089
+ this.auditComponent(session, policy, 'aggregate', 'fresh',
1090
+ policy.aggregateEnabled ? 'skipped' : 'disabled',
1091
+ !policy.aggregateEnabled ? 'profile-policy'
1092
+ : !exactAvailable ? 'exact-tokenizer-unavailable'
1093
+ : (aggregateInputTokens ?? 0) <= policy.aggregateTriggerTokens ? 'at-or-below-trigger'
1094
+ : aggregatePlanned === 0 ? 'no-valid-reduction'
1095
+ : 'recovery-tool-unavailable', {
1096
+ measurementKind: exactAvailable ? 'exact-tokenizer' : 'unavailable',
1097
+ ...(aggregateInputTokens === undefined ? {} : { currentTokens: aggregateInputTokens }),
1098
+ triggerTokens: policy.aggregateTriggerTokens,
1099
+ targetTokens: policy.aggregateTargetTokens,
1100
+ })
1101
+ }
1102
+ for (const candidate of candidates) decisions.add(candidate.seq)
1103
+ return summarize(landed)
1104
+ }
1105
+
1106
+ private snapshot(session: Session, view: CompactionTokenView): SnapshotCandidate[] {
1107
+ const events = sessionEvents(session)
1108
+ const calls = new Map<string, ToolCallInfo>()
1109
+ for (const event of events) {
1110
+ if (event.type === 'tool/call') {
1111
+ calls.set(event.data.callId, { name: event.data.name, arguments: event.data.arguments })
1112
+ }
1113
+ }
1114
+ const candidates: SnapshotCandidate[] = []
1115
+ const measured = new Map(view.measuredNodes.map(node => [node.seq, node.count]))
1116
+ const projectionPrices = new Map(view.nodes.map(node => [node.seq, node.tokens]))
1117
+ for (const seq of [...session.surface.nodes]) {
1118
+ const event = events[seq]
1119
+ if (event?.type !== 'tool/result') continue
1120
+ const shadowedHeuristicTokenCount = projectionPrices.get(seq)
1121
+ if (shadowedHeuristicTokenCount === undefined) {
1122
+ throw new Error(`surface node ${String(seq)} is absent from the atomic legacy projection`)
1123
+ }
1124
+ const content = event.data.message.content[0].content
1125
+ candidates.push({
1126
+ seq,
1127
+ event,
1128
+ call: calls.get(event.data.message.source.callId) ?? { name: 'unknown', arguments: '{}' },
1129
+ count: onlyTextBlocks(content) === null
1130
+ ? unavailableCount(`surface node ${String(seq)} contains unsupported rich tool-result content`)
1131
+ : measured.get(seq) ?? unavailableCount(`surface node ${String(seq)} is absent from the atomic token view`),
1132
+ shadowedHeuristicTokenCount,
1133
+ characterPressure: pressureCost(content),
1134
+ })
1135
+ }
1136
+ return candidates
1137
+ }
1138
+
1139
+ private planNative(
1140
+ candidate: SnapshotCandidate,
1141
+ session: Session,
1142
+ stage: PruneStage,
1143
+ policy: CompressionPolicy,
1144
+ view: CompactionTokenView,
1145
+ ): PlannedReplacement | null {
1146
+ if (this.isRecoveryExempt(session, candidate)) return null
1147
+ const tokensBefore = exactTokens(candidate.count)
1148
+ if (tokensBefore === undefined || tokensBefore <= policy.nativeTriggerTokens) return null
1149
+ const result = candidate.event.data.message.content[0]
1150
+ if (onlyTextBlocks(result.content) === null) return null
1151
+ const sourceSeq = rootToolResultSeq(session, candidate.seq)
1152
+ const marker = recoveryMarker(sourceRefFn(session, sourceSeq), 'tool result middle pruned')
1153
+ let head = this.state.config.headChars
1154
+ let tail = this.state.config.tailChars
1155
+ for (let attempt = 0; attempt < 10; attempt += 1) {
1156
+ const threshold = head + codePointLength(marker) + tail
1157
+ const content = nativePruneContent(result.content, threshold, head, tail, marker)
1158
+ if (content !== null) {
1159
+ const plan = this.plan(
1160
+ candidate,
1161
+ content,
1162
+ sourceSeq,
1163
+ 'native-head-tail',
1164
+ stage,
1165
+ 'native-tool-result',
1166
+ undefined,
1167
+ view,
1168
+ )
1169
+ if (plan !== null && plan.tokensAfter <= policy.nativeTargetTokens) return plan
1170
+ }
1171
+ if (head === 0 && tail === 0) break
1172
+ head = Math.floor(head / 2)
1173
+ tail = Math.floor(tail / 2)
1174
+ }
1175
+ return this.planAggregate(
1176
+ candidate,
1177
+ session,
1178
+ view,
1179
+ 'native-whole-result',
1180
+ stage,
1181
+ policy.nativeTargetTokens,
1182
+ 'native-tool-result',
1183
+ )
1184
+ }
1185
+
1186
+ /**
1187
+ * TokenPilot-inspired A1: replace a byte-identical repeat of an earlier
1188
+ * oversized tool result with a pointer to its first occurrence. The first
1189
+ * occurrence's hash is always recorded so later repeats can point at the
1190
+ * append-only original event even after the surface copy is reduced.
1191
+ */
1192
+ private planDedupe(
1193
+ candidate: SnapshotCandidate,
1194
+ session: Session,
1195
+ policy: CompressionPolicy,
1196
+ view: CompactionTokenView,
1197
+ ): PlannedReplacement | null {
1198
+ if (typeof candidate.event.surfaceOp === 'object') return null
1199
+ const result = candidate.event.data.message.content[0]
1200
+ const text = flattenPlainText(result.content)
1201
+ if (text === undefined) return null
1202
+ const tokensBefore = exactTokens(candidate.count)
1203
+ if (tokensBefore === undefined || tokensBefore <= policy.freshTriggerTokens) return null
1204
+ let table = this.state.dedupeTables.get(session)
1205
+ if (table === undefined) {
1206
+ table = new DedupeTable()
1207
+ this.state.dedupeTables.set(session, table)
1208
+ }
1209
+ const hash = dedupeHash(text, 'trim-eol')
1210
+ const entry = table.get(hash)
1211
+ if (entry !== undefined && entry.seq !== candidate.seq) {
1212
+ const placeholder = dedupePlaceholder(entry, codePointLength(text))
1213
+ const plan = this.plan(
1214
+ candidate,
1215
+ [{ type: 'text', text: placeholder }],
1216
+ entry.seq,
1217
+ 'dedupe-pointer',
1218
+ 'fresh',
1219
+ 'fresh',
1220
+ undefined,
1221
+ view,
1222
+ { noNetSavingsGuard: true },
1223
+ )
1224
+ if (plan !== null) return plan
1225
+ return null
1226
+ }
1227
+ if (entry === undefined) {
1228
+ table.record(hash, {
1229
+ seq: candidate.seq,
1230
+ sourceRef: sourceRefFn(session, candidate.seq),
1231
+ toolName: candidate.call.name,
1232
+ originalChars: codePointLength(text),
1233
+ })
1234
+ }
1235
+ return null
1236
+ }
1237
+
1238
+ private planFresh(
1239
+ candidate: SnapshotCandidate,
1240
+ session: Session,
1241
+ policy: CompressionPolicy,
1242
+ view: CompactionTokenView,
1243
+ ): PlannedReplacement | null {
1244
+ // A replace event already reflects one frozen first-exposure decision. The
1245
+ // pre-step coordinate filter prevents previously-kept originals from ever
1246
+ // being reconsidered after their first request.
1247
+ if (typeof candidate.event.surfaceOp === 'object') return null
1248
+ const result = candidate.event.data.message.content[0]
1249
+ const tokensBefore = exactTokens(candidate.count)
1250
+ if (tokensBefore === undefined || tokensBefore <= policy.freshTriggerTokens) return null
1251
+ const sourceSeq = candidate.seq
1252
+ const sourceRef = sourceRefFn(session, sourceSeq)
1253
+ const textBlock = onlyTextBlock(result.content)
1254
+ if (textBlock !== null) {
1255
+ let budgetChars = Math.max(1, Math.floor(codePointLength(textBlock.text) * 0.75))
1256
+ const codeSkeleton = this.activeSettings(session).codeSkeleton.enabled
1257
+ for (let attempt = 0; attempt < 10; attempt += 1) {
1258
+ const output = reduceFreshToolResult({
1259
+ toolName: candidate.call.name,
1260
+ argumentsText: candidate.call.arguments,
1261
+ text: textBlock.text,
1262
+ budgetChars,
1263
+ sourceRef,
1264
+ isError: result.isError === true || candidate.event.data.error !== undefined,
1265
+ codeSkeleton,
1266
+ })
1267
+ if (output !== null) {
1268
+ const plan = this.plan(
1269
+ candidate,
1270
+ [{ ...textBlock, text: output.text }],
1271
+ sourceSeq,
1272
+ output.reducer,
1273
+ 'fresh',
1274
+ 'fresh',
1275
+ undefined,
1276
+ view,
1277
+ { noNetSavingsGuard: policy.presetOptions?.noNetSavingsGuard === true },
1278
+ )
1279
+ if (plan !== null && plan.tokensAfter <= policy.freshTargetTokens) return plan
1280
+ }
1281
+ if (budgetChars === 1) break
1282
+ budgetChars = Math.max(1, Math.floor(budgetChars / 2))
1283
+ }
1284
+ }
1285
+
1286
+ return this.planAggregate(
1287
+ candidate,
1288
+ session,
1289
+ view,
1290
+ 'fresh-whole-result',
1291
+ 'fresh',
1292
+ policy.freshTargetTokens,
1293
+ 'fresh',
1294
+ )
1295
+ }
1296
+
1297
+ private planAggregate(
1298
+ candidate: SnapshotCandidate,
1299
+ session: Session,
1300
+ view: CompactionTokenView,
1301
+ reducer = 'fresh-step-aggregate',
1302
+ stage: PruneStage = 'fresh',
1303
+ targetTokens?: number,
1304
+ component: CompressionAuditComponent = 'aggregate',
1305
+ historyMode?: HistoryMode,
1306
+ ): PlannedReplacement | null {
1307
+ if (isError(candidate)) {
1308
+ return this.planErrorEvidence(
1309
+ candidate,
1310
+ session,
1311
+ view,
1312
+ stage,
1313
+ targetTokens,
1314
+ component,
1315
+ historyMode,
1316
+ )
1317
+ }
1318
+ const sourceSeq = rootToolResultSeq(session, candidate.seq)
1319
+ const sourceRef = sourceRefFn(session, sourceSeq)
1320
+ const text = [
1321
+ '[Tool result reduced to satisfy the completed-step aggregate budget]',
1322
+ `tool: ${candidate.call.name}`,
1323
+ `source: ${sourceRef}`,
1324
+ 'Use context_compression_retrieve with this source if the omitted evidence is necessary.',
1325
+ ].join('\n')
1326
+ const plan = this.plan(
1327
+ candidate,
1328
+ [{ type: 'text', text }],
1329
+ sourceSeq,
1330
+ reducer,
1331
+ stage,
1332
+ component,
1333
+ historyMode,
1334
+ view,
1335
+ )
1336
+ return plan !== null && (targetTokens === undefined || plan.tokensAfter <= targetTokens)
1337
+ ? plan
1338
+ : null
1339
+ }
1340
+
1341
+ /** Preserve bounded diagnostic evidence whenever an all-text error is reduced. */
1342
+ private planErrorEvidence(
1343
+ candidate: SnapshotCandidate,
1344
+ session: Session,
1345
+ view: CompactionTokenView,
1346
+ stage: PruneStage,
1347
+ targetTokens?: number,
1348
+ component: CompressionAuditComponent = 'aggregate',
1349
+ historyMode?: HistoryMode,
1350
+ ): PlannedReplacement | null {
1351
+ if (!isError(candidate)) return null
1352
+ const result = candidate.event.data.message.content[0]
1353
+ const blocks = onlyTextBlocks(result.content)
1354
+ if (blocks === null) return null
1355
+ const text = blocks.map(block => block.text).join('\n')
1356
+ const sourceSeq = rootToolResultSeq(session, candidate.seq)
1357
+ const sourceRef = sourceRefFn(session, sourceSeq)
1358
+ const output = historicalPlaceholder({
1359
+ toolName: candidate.call.name,
1360
+ sourceRef,
1361
+ charsBefore: codePointLength(text),
1362
+ isError: true,
1363
+ text,
1364
+ compact: false,
1365
+ })
1366
+ const input = {
1367
+ toolName: candidate.call.name,
1368
+ argumentsText: candidate.call.arguments,
1369
+ text,
1370
+ budgetChars: 1_200,
1371
+ sourceRef,
1372
+ isError: true,
1373
+ }
1374
+ if (!verifyReduction(input, output)) return null
1375
+ const plan = this.plan(
1376
+ candidate,
1377
+ [{ type: 'text', text: output.text }],
1378
+ sourceSeq,
1379
+ 'error-evidence-placeholder',
1380
+ stage,
1381
+ component,
1382
+ historyMode,
1383
+ view,
1384
+ )
1385
+ return plan !== null && (targetTokens === undefined || plan.tokensAfter <= targetTokens)
1386
+ ? plan
1387
+ : null
1388
+ }
1389
+
1390
+ private planHistoricalAging(
1391
+ session: Session,
1392
+ policy: CompressionPolicy,
1393
+ view: CompactionTokenView,
1394
+ ): HistoryPlanOutcome {
1395
+ const candidates = this.snapshot(session, view)
1396
+ const events = sessionEvents(session)
1397
+ const exact: number[] = []
1398
+ for (const candidate of candidates) {
1399
+ const tokens = exactTokens(candidate.count)
1400
+ if (tokens === undefined) {
1401
+ this.warnExactUnavailable(session, view, 'history')
1402
+ return { kind: 'exact-tokenizer-unavailable' }
1403
+ }
1404
+ exact.push(tokens)
1405
+ }
1406
+ const total = exact.reduce((sum, tokens) => sum + tokens, 0)
1407
+ const trigger = policy.historyTriggerTokens
1408
+ // Full-request last chance: ordinary prose, images, prompts, or schemas can
1409
+ // push the complete request past the Auto Compact deadline before the tool
1410
+ // results alone cross the profile trigger.
1411
+ const deadline = policy.microDeadlineTokens
1412
+ const lastChance = deadline !== undefined && view.totalTokens >= deadline
1413
+ if (total <= trigger && !lastChance) return { kind: 'below-profile-trigger' }
1414
+
1415
+ const protectedSeqs = this.protectedHistoryCandidateSeqs(candidates, policy)
1416
+ const isUnsafe = (candidate: SnapshotCandidate): boolean => {
1417
+ if (this.isRecoveryExempt(session, candidate)) return true
1418
+ const result = candidate.event.data.message.content[0]
1419
+ const block = onlyTextBlock(result.content)
1420
+ return block?.text.includes('[Old tool result content cleared from active context]') === true
1421
+ }
1422
+ // Distinguish "nothing safe to touch" (recovery tool output or already
1423
+ // cleared) from "everything left is inside the protected working set":
1424
+ // both skip, but they are different operational facts.
1425
+ const safe = candidates.filter(candidate => !isUnsafe(candidate))
1426
+ const eligible = safe.filter(candidate => !protectedSeqs.has(candidate.seq))
1427
+ if (eligible.length === 0) {
1428
+ return safe.length === 0
1429
+ ? { kind: 'no-safe-candidates' }
1430
+ : { kind: 'protected-working-set' }
1431
+ }
1432
+ const planned: PlannedReplacement[] = []
1433
+ let reclaim = 0
1434
+ // With a frozen Auto Compact deadline, microTarget = max(0, D - M) and one
1435
+ // batch must justify its cache break by pulling the complete request back
1436
+ // below the deadline. Without linkage (Custom manual, or no resolved
1437
+ // routed capacity) the loop keeps its traditional target and the batch
1438
+ // still commits once it reaches the minimum reclaim.
1439
+ const microTarget = deadline === undefined ? undefined : Math.max(0, deadline - policy.historyMinReclaimTokens)
1440
+ const required = Math.max(
1441
+ policy.historyMinReclaimTokens,
1442
+ total - trigger,
1443
+ ...(microTarget === undefined ? [] : [view.totalTokens - microTarget]),
1444
+ )
1445
+ const batchTarget = microTarget === undefined
1446
+ ? policy.historyMinReclaimTokens
1447
+ : required
1448
+ for (const candidate of eligible) {
1449
+ const result = candidate.event.data.message.content[0]
1450
+ const block = onlyTextBlock(result.content)
1451
+ // TokenPilot-inspired R2: a read output whose file was later mutated is
1452
+ // superseded — its text can no longer match the file — so it takes the
1453
+ // small whole-result placeholder before the ordinary reducer runs.
1454
+ if (policy.presetOptions?.readState === true && block !== null) {
1455
+ const readPath = toolCallPath(candidate.call.arguments)
1456
+ const estimatorExpired = this.state.estimatorVerdicts.get(session)?.get(candidate.seq) === true
1457
+ if (readPath !== undefined
1458
+ && (isSupersededRead(events, candidate.seq, readPath) || estimatorExpired)) {
1459
+ const plan = this.planAggregate(
1460
+ candidate,
1461
+ session,
1462
+ view,
1463
+ 'superseded-read-whole-result',
1464
+ 'pressure',
1465
+ undefined,
1466
+ 'history',
1467
+ policy.historyMode,
1468
+ )
1469
+ if (plan === null) continue
1470
+ planned.push(plan)
1471
+ reclaim += plan.tokensBefore - plan.tokensAfter
1472
+ if (reclaim >= required) break
1473
+ continue
1474
+ }
1475
+ }
1476
+ const sourceSeq = rootToolResultSeq(session, candidate.seq)
1477
+ if (block === null) {
1478
+ const plan = this.planAggregate(
1479
+ candidate,
1480
+ session,
1481
+ view,
1482
+ 'historical-rich-whole-result',
1483
+ 'pressure',
1484
+ undefined,
1485
+ 'history',
1486
+ policy.historyMode,
1487
+ )
1488
+ if (plan === null) continue
1489
+ planned.push(plan)
1490
+ reclaim += plan.tokensBefore - plan.tokensAfter
1491
+ if (reclaim >= required) break
1492
+ continue
1493
+ }
1494
+ const output = historicalPlaceholder({
1495
+ toolName: candidate.call.name,
1496
+ sourceRef: sourceRefFn(session, sourceSeq),
1497
+ charsBefore: codePointLength(block.text),
1498
+ isError: result.isError === true || candidate.event.data.error !== undefined,
1499
+ text: block.text,
1500
+ compact: false,
1501
+ })
1502
+ const verifyInput = {
1503
+ toolName: candidate.call.name,
1504
+ argumentsText: candidate.call.arguments,
1505
+ text: block.text,
1506
+ budgetChars: 1_200,
1507
+ sourceRef: sourceRefFn(session, sourceSeq),
1508
+ isError: result.isError === true || candidate.event.data.error !== undefined,
1509
+ }
1510
+ if (!verifyReduction(verifyInput, output)) continue
1511
+ // TokenPilot-inspired R3: when read-state semantics are on, append an
1512
+ // error/warn/info census of the omitted lines so the model keeps
1513
+ // meta-knowledge about what was dropped.
1514
+ let replacementText = output.text
1515
+ if (policy.presetOptions?.readState === true) {
1516
+ const omitted = countOmittedLines(block.text, output.text)
1517
+ const census = omitted === undefined ? undefined : clusterOmittedLines(block.text, omitted)
1518
+ if (census !== undefined) replacementText = `${output.text}
1519
+ [... ${census} ...]`
1520
+ }
1521
+ const plan = this.plan(
1522
+ candidate,
1523
+ [{ ...block, text: replacementText }],
1524
+ sourceSeq,
1525
+ output.reducer,
1526
+ 'pressure',
1527
+ 'history',
1528
+ policy.historyMode,
1529
+ view,
1530
+ )
1531
+ if (plan === null) continue
1532
+ planned.push(plan)
1533
+ reclaim += plan.tokensBefore - plan.tokensAfter
1534
+ if (reclaim >= required) break
1535
+ }
1536
+ // Linked batches must reach the deadline target; unlinked batches keep
1537
+ // the traditional minimum-reclaim commit threshold.
1538
+ if (reclaim >= batchTarget && planned.length > 0) return historyOutcome(planned)
1539
+ return lastChance
1540
+ ? { kind: 'cannot-reach-deadline-target', reclaim, required }
1541
+ : { kind: 'insufficient-reclaim', reclaim, required }
1542
+ }
1543
+
1544
+ private protectedHistoryResultSeqs(
1545
+ session: Session,
1546
+ policy: CompressionPolicy,
1547
+ view: CompactionTokenView,
1548
+ ): Set<number> | null {
1549
+ const candidates = this.snapshot(session, view)
1550
+ if (candidates.some(candidate => exactTokens(candidate.count) === undefined)) return null
1551
+ return this.protectedHistoryCandidateSeqs(candidates, policy)
1552
+ }
1553
+
1554
+ /** Select the newest completed tool calls and token tail for History-derived stages. */
1555
+ private protectedHistoryCandidateSeqs(
1556
+ candidates: readonly SnapshotCandidate[],
1557
+ policy: CompressionPolicy,
1558
+ ): Set<number> {
1559
+ const protectedSeqs = new Set<number>()
1560
+ for (let index = candidates.length - 1;
1561
+ index >= 0 && candidates.length - index <= policy.historyKeepRecentToolCalls;
1562
+ index--) {
1563
+ const candidate = candidates[index]
1564
+ if (candidate !== undefined) protectedSeqs.add(candidate.seq)
1565
+ }
1566
+ let recentTokens = 0
1567
+ for (let index = candidates.length - 1;
1568
+ index >= 0 && recentTokens < policy.historyKeepRecentTokens;
1569
+ index--) {
1570
+ const candidate = candidates[index]
1571
+ if (candidate === undefined) continue
1572
+ protectedSeqs.add(candidate.seq)
1573
+ recentTokens += exactTokens(candidate.count) ?? 0
1574
+ }
1575
+ return protectedSeqs
1576
+ }
1577
+
1578
+ /** Atomically replace at most one oldest safe completed tool-call group. */
1579
+ private landOldestTailTrimGroup(
1580
+ session: Session,
1581
+ policy: CompressionPolicy,
1582
+ view: CompactionTokenView,
1583
+ ): void {
1584
+ const tailTrim = policy.tailTrim
1585
+ if (tailTrim?.enabled !== true) return
1586
+ const events = sessionEvents(session)
1587
+ if (view.currentSurface.kind !== 'exact-tokenizer'
1588
+ || view.currentSurface.tokens <= tailTrim.triggerTokens) {
1589
+ if (view.currentSurface.kind !== 'exact-tokenizer') this.warnExactUnavailable(session, view, 'tailtrim')
1590
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
1591
+ view.currentSurface.kind !== 'exact-tokenizer'
1592
+ ? 'exact-tokenizer-unavailable' : 'at-or-below-trigger', {
1593
+ measurementKind: view.currentSurface.kind,
1594
+ ...(view.currentSurface.kind === 'exact-tokenizer'
1595
+ ? { currentTokens: view.currentSurface.tokens }
1596
+ : {}),
1597
+ triggerTokens: tailTrim.triggerTokens,
1598
+ })
1599
+ return
1600
+ }
1601
+ const surfaceCount = view.currentSurface
1602
+ if (!this.hasRecoveryTool(session)) {
1603
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
1604
+ 'recovery-tool-unavailable', {
1605
+ measurementKind: 'exact-tokenizer',
1606
+ currentTokens: surfaceCount.tokens,
1607
+ triggerTokens: tailTrim.triggerTokens,
1608
+ })
1609
+ return
1610
+ }
1611
+ if (!hasOpenTurn(session)) {
1612
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
1613
+ 'no-open-turn', {
1614
+ measurementKind: 'exact-tokenizer',
1615
+ currentTokens: surfaceCount.tokens,
1616
+ triggerTokens: tailTrim.triggerTokens,
1617
+ })
1618
+ return
1619
+ }
1620
+ const protectedResults = this.protectedHistoryResultSeqs(session, policy, view)
1621
+ if (protectedResults === null) {
1622
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
1623
+ 'exact-tokenizer-unavailable-in-protected-set', {
1624
+ measurementKind: 'unavailable',
1625
+ currentTokens: surfaceCount.tokens,
1626
+ triggerTokens: tailTrim.triggerTokens,
1627
+ })
1628
+ return
1629
+ }
1630
+ const measured = new Map(view.measuredNodes.map(node => [node.seq, node.count]))
1631
+ const heuristic = new Map(view.nodes.map(node => [node.seq, node.tokens]))
1632
+ const completedTurns = new Set<number>()
1633
+ const completedSteps = new Set<string>()
1634
+ for (const event of events) {
1635
+ if (event.type === 'turn/end') completedTurns.add(event.data.turn)
1636
+ else if (event.type === 'step/end') completedSteps.add(`${String(event.data.turn)}:${String(event.data.step)}`)
1637
+ }
1638
+ const firstCompletedSurfaceTurn = session.surface.nodes
1639
+ .map(seq => events[seq])
1640
+ .filter((event): event is SessionEvent<'assistant/message'> | SessionEvent<'tool/result'> =>
1641
+ (event?.type === 'assistant/message' || event?.type === 'tool/result')
1642
+ && completedTurns.has(event.data.turn))
1643
+ .reduce<number | undefined>(
1644
+ (first, event) => first === undefined ? event.data.turn : Math.min(first, event.data.turn),
1645
+ undefined,
1646
+ )
1647
+ const nodes = [...session.surface.nodes]
1648
+ for (let index = 0; index < nodes.length; index++) {
1649
+ const assistantSeq = nodes[index]
1650
+ if (assistantSeq === undefined) continue
1651
+ const assistant = events[assistantSeq]
1652
+ if (assistant?.type !== 'assistant/message'
1653
+ || assistant.data.interrupted === true
1654
+ || assistant.data.message.content.length === 0
1655
+ || assistant.data.message.content.some(block => block.type !== 'tool-call')
1656
+ || assistant.data.turn === firstCompletedSurfaceTurn
1657
+ || !completedTurns.has(assistant.data.turn)
1658
+ || !completedSteps.has(`${String(assistant.data.turn)}:${String(assistant.data.step)}`)) continue
1659
+ const calls = assistant.data.message.content as Extract<ContentBlock, { type: 'tool-call' }>[]
1660
+ if (calls.some(call => call.name === 'context_compression_retrieve')) continue
1661
+ const callIds = calls.map(call => String(call.id))
1662
+ if (new Set(callIds).size !== callIds.length) continue
1663
+ const resultSeqs = nodes.slice(index + 1, index + 1 + calls.length)
1664
+ if (resultSeqs.length !== calls.length || resultSeqs.some(seq => protectedResults.has(seq))) continue
1665
+ const results = resultSeqs.map(seq => events[seq])
1666
+ if (results.some((event): boolean => {
1667
+ if (event?.type !== 'tool/result'
1668
+ || event.data.turn !== assistant.data.turn || event.data.step !== assistant.data.step
1669
+ || event.data.error !== undefined) return true
1670
+ const block = event.data.message.content[0]
1671
+ if (block.isError === true) return true
1672
+ // Images and other rich inner blocks stay fail-open: a TailTrim stub
1673
+ // would silently delete them from the active context.
1674
+ return block.content.some(contentBlock => contentBlock.type !== 'text')
1675
+ })) continue
1676
+ const next = events[nodes[index + 1 + calls.length] ?? -1]
1677
+ if (next?.type === 'tool/result'
1678
+ && next.data.turn === assistant.data.turn
1679
+ && next.data.step === assistant.data.step) continue
1680
+ const resultIds = results.map(event => event?.type === 'tool/result'
1681
+ ? String(event.data.message.source.callId) : '')
1682
+ if (new Set(resultIds).size !== resultIds.length
1683
+ || resultIds.some((id, resultIndex) => id !== callIds[resultIndex])) continue
1684
+ const shadowedSeqs = [assistantSeq, ...resultSeqs]
1685
+ const roots = shadowedSeqs.map(seq => this.uniqueAppendRoot(session, seq))
1686
+ if (roots.some(root => root === null)) continue
1687
+ const sourceEventSeqs = roots as number[]
1688
+ if (new Set(sourceEventSeqs).size !== sourceEventSeqs.length) continue
1689
+ const counts = shadowedSeqs.map(seq => measured.get(seq))
1690
+ if (counts.some(count => count?.kind !== 'exact-tokenizer')) continue
1691
+ const exactCounts = counts as Extract<TokenCount, { kind: 'exact-tokenizer' }>[]
1692
+ if (exactCounts.some(count => count.tokenizerId !== surfaceCount.tokenizerId
1693
+ || count.tokenizerRevision !== surfaceCount.tokenizerRevision)) continue
1694
+ const tokensBefore = exactCounts.reduce((sum, count) => sum + count.tokens, 0)
1695
+ const manifestSeq = events.length
1696
+ const ref = tailTrimRef(String(session.id), manifestSeq)
1697
+ const stub = tailTrimStub(ref, calls.map(call => call.name), sourceEventSeqs)
1698
+ if (stub === null) continue
1699
+ const stubCount = countExactCanonicalTextFields(
1700
+ [stub],
1701
+ candidate => view.countCanonicalText(candidate),
1702
+ 'TailTrim group stub',
1703
+ )
1704
+ if (stubCount.kind !== 'exact-tokenizer'
1705
+ || stubCount.tokenizerId !== surfaceCount.tokenizerId
1706
+ || stubCount.tokenizerRevision !== surfaceCount.tokenizerRevision
1707
+ || stubCount.tokens <= 0
1708
+ || tokensBefore - stubCount.tokens < policy.historyMinReclaimTokens) continue
1709
+ const heuristicTokens = shadowedSeqs.reduce((sum, seq) => sum + (heuristic.get(seq) ?? 0), 0)
1710
+ const range = { start: assistantSeq, end: resultSeqs.at(-1) ?? assistantSeq }
1711
+ if (!this.reserveTailTrimBoundaryAttempt(session)) {
1712
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
1713
+ 'already-attempted-at-request-boundary', {
1714
+ measurementKind: 'exact-tokenizer',
1715
+ currentTokens: surfaceCount.tokens,
1716
+ triggerTokens: tailTrim.triggerTokens,
1717
+ })
1718
+ return
1719
+ }
1720
+ const manifest = session.append('compaction/prune', {
1721
+ shadowedRange: range,
1722
+ shadowedSeqs,
1723
+ shadowedTokenCount: heuristicTokens,
1724
+ })
1725
+ let replacement: SessionEvent<'user/message'>
1726
+ try {
1727
+ replacement = session.append('user/message', tailTrimMessage(stub), {
1728
+ surfaceOp: { op: 'replace', ...range },
1729
+ sourceEventSeqs: [manifest.seq, ...shadowedSeqs],
1730
+ })
1731
+ } catch (error) {
1732
+ this.auditPublicationFailure(
1733
+ session,
1734
+ 'pressure',
1735
+ 'tail-trim',
1736
+ manifest.seq,
1737
+ error,
1738
+ )
1739
+ return
1740
+ }
1741
+ emitCompressionAudit(this.ctx.logger, {
1742
+ schemaVersion: 1,
1743
+ kind: 'rewrite',
1744
+ sessionId: String(session.id),
1745
+ profile: policy.profile,
1746
+ component: 'tail-trim',
1747
+ stage: 'pressure',
1748
+ reducer: 'pair-preserving-tail-trim',
1749
+ manifestEventType: 'compaction/prune',
1750
+ manifestSeq: manifest.seq,
1751
+ replacementSeq: replacement.seq,
1752
+ sourceSeqs: sourceEventSeqs,
1753
+ tokensBefore,
1754
+ tokensAfter: stubCount.tokens,
1755
+ tokensRemoved: tokensBefore - stubCount.tokens,
1756
+ tokenizerId: stubCount.tokenizerId,
1757
+ tokenizerRevision: stubCount.tokenizerRevision,
1758
+ })
1759
+ return
1760
+ }
1761
+ this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
1762
+ 'no-safe-eligible-tool-group', {
1763
+ measurementKind: 'exact-tokenizer',
1764
+ currentTokens: surfaceCount.tokens,
1765
+ triggerTokens: tailTrim.triggerTokens,
1766
+ })
1767
+ }
1768
+
1769
+ private reserveTailTrimBoundaryAttempt(session: Session): boolean {
1770
+ const boundary = this.state.activeRequestBoundaries.get(session)
1771
+ if (boundary === undefined) return true
1772
+ if (this.state.tailTrimBoundaryAttempts.get(session) === boundary) return false
1773
+ this.state.tailTrimBoundaryAttempts.set(session, boundary)
1774
+ return true
1775
+ }
1776
+
1777
+ private uniqueAppendRoot(session: Session, seq: number): number | null {
1778
+ const events = sessionEvents(session)
1779
+ const pending: Array<{ seq: number; depth: number }> = [{ seq, depth: 0 }]
1780
+ const visited = new Set<number>()
1781
+ const roots = new Set<number>()
1782
+ while (pending.length > 0) {
1783
+ const next = pending.pop()
1784
+ if (next === undefined || next.depth > 64 || visited.has(next.seq)) continue
1785
+ visited.add(next.seq)
1786
+ if (visited.size > 64) return null
1787
+ const event = events[next.seq]
1788
+ if (event === undefined || (event.type !== 'assistant/message' && event.type !== 'tool/result')) return null
1789
+ if (event.surfaceOp === 'append') roots.add(event.seq)
1790
+ else if (typeof event.surfaceOp === 'object') {
1791
+ const sources = event.sourceEventSeqs
1792
+ if (sources === undefined || sources.length === 0) return null
1793
+ for (const source of sources) pending.push({ seq: source, depth: next.depth + 1 })
1794
+ } else return null
1795
+ if (roots.size > 1) return null
1796
+ }
1797
+ return roots.size === 1 ? [...roots][0] ?? null : null
1798
+ }
1799
+
1800
+ private plan(
1801
+ candidate: SnapshotCandidate,
1802
+ content: ContentBlock[],
1803
+ sourceSeq: number,
1804
+ reducer: string,
1805
+ stage: PruneStage,
1806
+ component: CompressionAuditComponent,
1807
+ historyMode: HistoryMode | undefined,
1808
+ view: CompactionTokenView,
1809
+ options: { readonly noNetSavingsGuard?: boolean } = {},
1810
+ ): PlannedReplacement | null {
1811
+ const countBefore = candidate.count
1812
+ if (countBefore.kind !== 'exact-tokenizer') return null
1813
+ const countAfter = countToolContent(content, view)
1814
+ if (countAfter.kind !== 'exact-tokenizer'
1815
+ || countAfter.tokenizerId !== countBefore.tokenizerId
1816
+ || countAfter.tokenizerRevision !== countBefore.tokenizerRevision) return null
1817
+ const tokensBefore = countBefore.tokens
1818
+ const tokensAfter = countAfter.tokens
1819
+ if (tokensAfter <= 0 || tokensAfter >= tokensBefore) return null
1820
+ // TokenPilot-style no-net-savings: even when the exact tokenizer reports a
1821
+ // saving, a replacement whose text is not smaller than its original adds
1822
+ // noise without reclaiming context. Text-level because the placeholder
1823
+ // guidance lines (source refs, retrieval hints) must pay for themselves.
1824
+ if (options.noNetSavingsGuard === true) {
1825
+ const originalBlocks = onlyTextBlocks(candidate.event.data.message.content[0].content)
1826
+ const replacementBlocks = onlyTextBlocks(content)
1827
+ if (originalBlocks !== null && replacementBlocks !== null) {
1828
+ const originalChars = originalBlocks.reduce((sum, block) => sum + codePointLength(block.text), 0)
1829
+ const replacementChars = replacementBlocks.reduce((sum, block) => sum + codePointLength(block.text), 0)
1830
+ if (replacementChars >= originalChars) return null
1831
+ }
1832
+ }
1833
+ const charsBefore = candidate.characterPressure
1834
+ const charsAfter = pressureCost(content)
1835
+ return {
1836
+ candidate,
1837
+ content,
1838
+ sourceSeq,
1839
+ reducer,
1840
+ stage,
1841
+ component,
1842
+ ...historyMode === undefined ? {} : { historyMode },
1843
+ charsBefore,
1844
+ charsAfter,
1845
+ tokensBefore,
1846
+ tokensAfter,
1847
+ tokenizerId: countBefore.tokenizerId,
1848
+ tokenizerRevision: countBefore.tokenizerRevision,
1849
+ }
1850
+ }
1851
+
1852
+ private land(session: Session, plan: PlannedReplacement): PrunedEntry | null {
1853
+ const { candidate } = plan
1854
+ const result = candidate.event.data.message.content[0]
1855
+ const message = freezeMessage<ToolResultMessage>({
1856
+ ...candidate.event.data.message,
1857
+ content: [{ ...result, content: plan.content }] as [typeof result],
1858
+ })
1859
+ const manifest = session.append('compaction/prune', {
1860
+ shadowedRange: { start: candidate.seq, end: candidate.seq },
1861
+ shadowedSeqs: [candidate.seq],
1862
+ shadowedTokenCount: candidate.shadowedHeuristicTokenCount,
1863
+ })
1864
+ let replacement: SessionEvent<'tool/result'>
1865
+ try {
1866
+ replacement = session.append('tool/result', {
1867
+ ...candidate.event.data,
1868
+ message,
1869
+ }, {
1870
+ surfaceOp: { op: 'replace', start: candidate.seq, end: candidate.seq },
1871
+ sourceEventSeqs: [candidate.seq],
1872
+ })
1873
+ } catch (error) {
1874
+ this.auditPublicationFailure(
1875
+ session,
1876
+ plan.stage,
1877
+ plan.component,
1878
+ manifest.seq,
1879
+ error,
1880
+ )
1881
+ return null
1882
+ }
1883
+ emitCompressionAudit(this.ctx.logger, {
1884
+ schemaVersion: 1,
1885
+ kind: 'rewrite',
1886
+ sessionId: String(session.id),
1887
+ profile: this.activeSettings(session).profile,
1888
+ component: plan.component,
1889
+ stage: plan.stage,
1890
+ reducer: plan.reducer,
1891
+ ...plan.historyMode === undefined ? {} : { historyMode: plan.historyMode },
1892
+ manifestEventType: 'compaction/prune',
1893
+ manifestSeq: manifest.seq,
1894
+ replacementSeq: replacement.seq,
1895
+ sourceSeqs: [plan.sourceSeq],
1896
+ tokensBefore: plan.tokensBefore,
1897
+ tokensAfter: plan.tokensAfter,
1898
+ tokensRemoved: plan.tokensBefore - plan.tokensAfter,
1899
+ tokenizerId: plan.tokenizerId,
1900
+ tokenizerRevision: plan.tokenizerRevision,
1901
+ })
1902
+ return {
1903
+ originalSeq: candidate.seq,
1904
+ sourceSeq: plan.sourceSeq,
1905
+ replacementSeq: replacement.seq,
1906
+ callId: candidate.event.data.message.source.callId,
1907
+ reducer: plan.reducer,
1908
+ stage: plan.stage,
1909
+ charsBefore: plan.charsBefore,
1910
+ charsAfter: plan.charsAfter,
1911
+ tokensBefore: plan.tokensBefore,
1912
+ tokensAfter: plan.tokensAfter,
1913
+ }
1914
+ }
1915
+
1916
+ private landAll(session: Session, plans: readonly PlannedReplacement[]): PrunedEntry[] {
1917
+ if (plans.length === 0) return []
1918
+ if (!this.hasRecoveryTool(session)) {
1919
+ this.warnOnce(
1920
+ session,
1921
+ 'missing-context-retrieve',
1922
+ 'context-compression kept original tool results because context_compression_retrieve is unavailable',
1923
+ )
1924
+ return []
1925
+ }
1926
+ if (!hasOpenTurn(session)) {
1927
+ throw new Error('tool-result pruning cannot append a surface replacement outside any open turn')
1928
+ }
1929
+ const landed: PrunedEntry[] = []
1930
+ for (const plan of plans) {
1931
+ const entry = this.land(session, plan)
1932
+ if (entry === null) break
1933
+ landed.push(entry)
1934
+ }
1935
+ return landed
1936
+ }
1937
+
1938
+ private hasRecoveryTool(session: Session): boolean {
1939
+ const tools = this.ctx.get('tools')
1940
+ if (tools === undefined) return false
1941
+ const agent = this.ctx.get('agents')?.get(session.id)
1942
+ return tools.get('context_compression_retrieve', agent) !== undefined
1943
+ }
1944
+
1945
+ private auditHistoryEvaluation(
1946
+ session: Session,
1947
+ policy: CompressionPolicy,
1948
+ view: CompactionTokenView,
1949
+ allowed: boolean,
1950
+ outcome: HistoryPlanOutcome,
1951
+ ): void {
1952
+ if (policy.historyMode === 'disabled') {
1953
+ this.auditComponent(session, policy, 'history', 'pressure', 'disabled', 'profile-policy', {
1954
+ historyMode: policy.historyMode,
1955
+ })
1956
+ return
1957
+ }
1958
+ if (!allowed && outcome.kind === 'planned') {
1959
+ // The authority gate itself refused: below the frozen micro deadline
1960
+ // (capacity-pressure) or the adaptive cost estimate rejected the batch.
1961
+ const deadlineTrigger = policy.microDeadlineTokens
1962
+ const capacity = deadlineTrigger === undefined ? session.requestContext()?.contextWindow : undefined
1963
+ const capacityTrigger = deadlineTrigger !== undefined
1964
+ ? deadlineTrigger
1965
+ : Number.isSafeInteger(capacity) && capacity !== undefined && capacity > 0
1966
+ ? Math.floor(capacity * CAPACITY_PRESSURE_RATIO)
1967
+ : undefined
1968
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
1969
+ policy.historyMode === 'capacity-pressure'
1970
+ ? 'below-micro-deadline' : 'adaptive-cost-rejected', {
1971
+ historyMode: policy.historyMode,
1972
+ measurementKind: view.currentSurface.kind,
1973
+ currentTokens: view.totalTokens,
1974
+ ...(capacityTrigger === undefined ? {} : { triggerTokens: capacityTrigger }),
1975
+ })
1976
+ return
1977
+ }
1978
+ const deadline = policy.microDeadlineTokens
1979
+ const lastChance = deadline !== undefined && view.totalTokens >= deadline
1980
+ const detail = (extra: Readonly<Record<string, number>> = {}): Readonly<{
1981
+ historyMode?: HistoryMode
1982
+ measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'unavailable'
1983
+ currentTokens?: number
1984
+ triggerTokens?: number
1985
+ reclaimTokens?: number
1986
+ requiredTokens?: number
1987
+ }> => ({
1988
+ historyMode: policy.historyMode,
1989
+ measurementKind: outcome.kind === 'exact-tokenizer-unavailable' ? 'unavailable' : 'exact-tokenizer',
1990
+ currentTokens: view.totalTokens,
1991
+ ...(outcome.kind === 'insufficient-reclaim' || outcome.kind === 'cannot-reach-deadline-target'
1992
+ ? { reclaimTokens: outcome.reclaim, requiredTokens: outcome.required }
1993
+ : {}),
1994
+ ...extra,
1995
+ })
1996
+ switch (outcome.kind) {
1997
+ case 'exact-tokenizer-unavailable':
1998
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
1999
+ 'exact-tokenizer-unavailable', detail({ triggerTokens: policy.historyTriggerTokens }))
2000
+ return
2001
+ case 'below-profile-trigger':
2002
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
2003
+ 'below-profile-trigger', detail({ triggerTokens: policy.historyTriggerTokens }))
2004
+ return
2005
+ case 'no-safe-candidates':
2006
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
2007
+ 'no-safe-candidates', detail({ triggerTokens: policy.historyTriggerTokens }))
2008
+ return
2009
+ case 'protected-working-set':
2010
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
2011
+ 'protected-working-set', detail({ triggerTokens: policy.historyTriggerTokens }))
2012
+ return
2013
+ case 'insufficient-reclaim':
2014
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
2015
+ 'insufficient-reclaim', detail({ triggerTokens: policy.historyTriggerTokens }))
2016
+ return
2017
+ case 'cannot-reach-deadline-target':
2018
+ // Planning engaged through the full-request last-chance gate; the
2019
+ // routine trigger numbers below would misdescribe why nothing landed.
2020
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
2021
+ 'cannot-reach-deadline-target', detail(lastChance ? { triggerTokens: deadline } : {}))
2022
+ return
2023
+ case 'planned':
2024
+ // A committed batch that landed nothing means the recovery tool was
2025
+ // unavailable at landing time; a degenerate empty commit reads as an
2026
+ // unreachable target instead.
2027
+ this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
2028
+ outcome.plans.length > 0 ? 'recovery-tool-unavailable' : 'insufficient-reclaim',
2029
+ detail({ triggerTokens: lastChance ? deadline : policy.historyTriggerTokens }))
2030
+ return
2031
+ /* v8 ignore next -- closed-union exhaustiveness guard */
2032
+ default:
2033
+ return assertNever(outcome, 'history plan outcome')
2034
+ }
2035
+ }
2036
+
2037
+ private auditComponent(
2038
+ session: Session,
2039
+ policy: CompressionPolicy,
2040
+ component: CompressionAuditComponent,
2041
+ stage: PruneStage,
2042
+ status: CompressionAuditEvaluationStatus,
2043
+ reason: string,
2044
+ detail: Readonly<{
2045
+ historyMode?: HistoryMode
2046
+ measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'unavailable'
2047
+ currentTokens?: number
2048
+ triggerTokens?: number
2049
+ targetTokens?: number
2050
+ reclaimTokens?: number
2051
+ requiredTokens?: number
2052
+ }> = {},
2053
+ ): void {
2054
+ emitCompressionAudit(this.ctx.logger, {
2055
+ schemaVersion: 1,
2056
+ kind: 'component-evaluation',
2057
+ sessionId: String(session.id),
2058
+ profile: policy.profile,
2059
+ component,
2060
+ stage,
2061
+ status,
2062
+ reason,
2063
+ ...detail,
2064
+ })
2065
+ }
2066
+
2067
+ private auditFailure(
2068
+ session: Session,
2069
+ stage: PruneStage,
2070
+ operation:
2071
+ | 'request-boundary'
2072
+ | 'terminal-pass'
2073
+ | 'policy-resolution'
2074
+ | 'summary-locator'
2075
+ | 'publication',
2076
+ error: unknown,
2077
+ ): void {
2078
+ emitCompressionAudit(this.ctx.logger, {
2079
+ schemaVersion: 1,
2080
+ kind: 'failure',
2081
+ sessionId: String(session.id),
2082
+ stage,
2083
+ operation,
2084
+ errorName: error instanceof Error ? error.name : 'UnknownError',
2085
+ errorMessage: error instanceof Error ? error.message : String(error),
2086
+ })
2087
+ }
2088
+
2089
+ private auditPublicationFailure(
2090
+ session: Session,
2091
+ stage: PruneStage,
2092
+ component: CompressionAuditComponent,
2093
+ manifestSeq: number,
2094
+ error: unknown,
2095
+ ): void {
2096
+ emitCompressionAudit(this.ctx.logger, {
2097
+ schemaVersion: 1,
2098
+ kind: 'failure',
2099
+ sessionId: String(session.id),
2100
+ stage,
2101
+ operation: 'publication',
2102
+ component,
2103
+ manifestSeq,
2104
+ errorName: error instanceof Error ? error.name : 'UnknownError',
2105
+ errorMessage: 'surface replacement append failed after compaction/prune committed',
2106
+ })
2107
+ }
2108
+
2109
+ private warnExactUnavailable(
2110
+ session: Session,
2111
+ view: CompactionTokenView,
2112
+ gate: 'native' | 'fresh' | 'aggregate' | 'history' | 'tailtrim',
2113
+ ): void {
2114
+ const provider = view.providerRoute ?? 'unbound-provider'
2115
+ const model = view.modelId ?? 'unbound-model'
2116
+ this.warnOnce(
2117
+ session,
2118
+ `exact-tokenizer:${gate}:${provider}\0${model}`,
2119
+ 'context-compression %s kept original tool results because exact tokenizer counts are unavailable for %s/%s',
2120
+ gate,
2121
+ provider,
2122
+ model,
2123
+ )
2124
+ }
2125
+
2126
+ private warnOnce(
2127
+ session: Session,
2128
+ key: string,
2129
+ message: string,
2130
+ ...args: unknown[]
2131
+ ): void {
2132
+ let warned = this.state.warnedFailures.get(session)
2133
+ if (warned === undefined) {
2134
+ warned = new Set()
2135
+ this.state.warnedFailures.set(session, warned)
2136
+ }
2137
+ if (warned.has(key)) return
2138
+ warned.add(key)
2139
+ this.ctx.logger.warn(message, ...args)
2140
+ }
2141
+
2142
+ }
2143
+
2144
+ export default ToolResultPruner