dsh-context-compression-improved 0.5.2 → 0.5.3

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