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