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.
- package/.gitattributes +1 -0
- package/CHANGELOG.ja.md +144 -119
- package/CHANGELOG.ko.md +143 -118
- package/CHANGELOG.md +278 -250
- package/CHANGELOG.zh.md +131 -109
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- package/packages/selector/src/client/EstimatorControls.tsx +277 -277
- package/packages/selector/src/client/locales.ts +196 -196
- package/packages/selector/src/index.ts +463 -463
- package/packages/selector/src/pruner/state.ts +50 -50
- package/packages/selector/src/pruner.ts +2402 -2402
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -149
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -200
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -232
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -96
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -217
- package/packages/selector/tests/settings-seat.client.spec.ts +29 -4
- 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
|