dsh-context-compression-improved 0.4.0-beta.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +68 -36
- package/CHANGELOG.ko.md +67 -35
- package/CHANGELOG.md +195 -134
- package/CHANGELOG.zh.md +64 -36
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/installation.ja.md +2 -2
- package/docs/installation.ko.md +2 -2
- package/docs/installation.md +103 -78
- package/docs/installation.zh.md +100 -77
- package/docs/repair-log.md +54 -0
- package/package.json +1 -1
- package/packages/selector/lib/{config.js → advisor-state.js} +329 -5
- package/packages/selector/lib/client.d.ts +7 -0
- package/packages/selector/lib/client.js +33 -3
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +112 -3
- package/packages/selector/lib/pruner.d.ts +128 -1
- package/packages/selector/lib/pruner.js +2802 -1374
- package/packages/selector/src/client/ReviewOverlay.tsx +1 -1
- package/packages/selector/src/client/index.ts +1 -1
- package/packages/selector/src/client/preset-options.ts +2 -0
- package/packages/selector/src/index.ts +129 -49
- package/packages/selector/src/profiles.ts +48 -0
- package/packages/selector/src/pruner/content.ts +18 -5
- package/packages/selector/src/pruner/state.ts +3 -0
- package/packages/selector/src/pruner/types.ts +23 -5
- package/packages/selector/src/pruner.ts +297 -162
- package/packages/selector/src/runtime/adaptive-cost.ts +23 -12
- package/packages/selector/src/runtime/audit.ts +40 -2
- package/packages/selector/src/runtime/config.ts +88 -1
- package/packages/selector/src/runtime/measurement.ts +31 -2
- package/packages/selector/src/runtime/reducers.ts +1115 -97
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +1 -1
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +8 -118
- package/packages/selector/src/runtime/tokenpilot/locator.ts +1 -1
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +76 -32
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +23 -2
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +117 -0
- package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +303 -0
- package/packages/selector/src/runtime/toolclass.ts +103 -0
- package/packages/selector/src/runtime/types.ts +37 -0
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +2 -1
- package/packages/selector/tests/review-routes-registry.host.spec.ts +142 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +7 -7
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
- package/packages/selector/tests/runtime/audit.spec.ts +88 -1
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +14 -3
- package/packages/selector/tests/runtime/frequency-longstrings.spec.ts +74 -0
- package/packages/selector/tests/runtime/html-reducer.spec.ts +212 -0
- package/packages/selector/tests/runtime/line-mapping.spec.ts +153 -0
- package/packages/selector/tests/runtime/prose-reducers.spec.ts +133 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +198 -27
- package/packages/selector/tests/runtime/read-input-cap.spec.ts +33 -0
- package/packages/selector/tests/runtime/search-reducer.spec.ts +110 -0
- package/packages/selector/tests/runtime/sidechannel.spec.ts +241 -0
- package/packages/selector/tests/runtime/toc-and-bundled.spec.ts +159 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +194 -0
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +70 -1
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +24 -0
- package/packages/selector/tests/runtime/toolclass.spec.ts +156 -0
- package/scripts/toolclass-corpus-replay.mjs +281 -0
|
@@ -21,7 +21,6 @@ import type {} from '@deepseek-ai/dsh-settings'
|
|
|
21
21
|
import type {
|
|
22
22
|
CompactionTokenView,
|
|
23
23
|
ObservedPromptUsage,
|
|
24
|
-
TokenCount,
|
|
25
24
|
} from './runtime/measurement.ts'
|
|
26
25
|
import { measureForCompaction } from './runtime/measurement.ts'
|
|
27
26
|
import { eventBySeq, sessionEvents } from './runtime/session-events.ts'
|
|
@@ -40,7 +39,6 @@ import {
|
|
|
40
39
|
onlyTextBlock,
|
|
41
40
|
onlyTextBlocks,
|
|
42
41
|
countToolContent,
|
|
43
|
-
exactTokens,
|
|
44
42
|
sameProviderMeasurementKey,
|
|
45
43
|
unavailableCount,
|
|
46
44
|
recoveryMarker,
|
|
@@ -76,13 +74,20 @@ import {
|
|
|
76
74
|
contentDigest,
|
|
77
75
|
} from './runtime/tokenpilot/proposal.ts'
|
|
78
76
|
import {
|
|
79
|
-
MemoryReviewStore,
|
|
80
77
|
ReviewQueue,
|
|
81
78
|
type ReviewProposalRecord,
|
|
82
|
-
type ReviewQueueStore,
|
|
83
79
|
type ReviewReceipt,
|
|
84
80
|
} from './runtime/tokenpilot/review-queue.ts'
|
|
81
|
+
import { registerReviewPruner, sharedReviewStore } from './runtime/tokenpilot/review-registry.ts'
|
|
85
82
|
import { openReviewStorage } from './runtime/tokenpilot/review-storage.ts'
|
|
83
|
+
import { SideChannel } from './runtime/tokenpilot/sidechannel.ts'
|
|
84
|
+
import {
|
|
85
|
+
advisorCandidatePreview,
|
|
86
|
+
collectTailText,
|
|
87
|
+
collectTaskSemantics,
|
|
88
|
+
runSessionAdvisorPass,
|
|
89
|
+
} from './runtime/tokenpilot/advisor.ts'
|
|
90
|
+
import { getAdvisorState } from './runtime/tokenpilot/advisor-state.ts'
|
|
86
91
|
|
|
87
92
|
import {
|
|
88
93
|
DedupeTable,
|
|
@@ -91,6 +96,8 @@ import {
|
|
|
91
96
|
flattenPlainText,
|
|
92
97
|
} from './runtime/tokenpilot/dedup.ts'
|
|
93
98
|
import {
|
|
99
|
+
charsForTokens,
|
|
100
|
+
charsToTokens,
|
|
94
101
|
codePointLength,
|
|
95
102
|
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
96
103
|
ContextCompressionSettingsSchema,
|
|
@@ -234,13 +241,23 @@ export class ToolResultPruner extends Service {
|
|
|
234
241
|
activeRequestBoundaries: new WeakMap(),
|
|
235
242
|
tailTrimBoundaryAttempts: new WeakMap(),
|
|
236
243
|
policyResolutionAudits: new WeakMap(),
|
|
237
|
-
reviewStore:
|
|
244
|
+
reviewStore: sharedReviewStore(),
|
|
238
245
|
reviewQueues: new WeakMap(),
|
|
239
246
|
reviewClocks: new WeakMap(),
|
|
240
247
|
estimatorRemainingTurns: new WeakMap(),
|
|
248
|
+
advisorChannels: new WeakMap(),
|
|
241
249
|
reviewSummaries: new WeakMap(),
|
|
242
250
|
}
|
|
243
251
|
|
|
252
|
+
// Publish this instance for the top-level R4 routes. They are registered on
|
|
253
|
+
// the plugin's top-level fiber while every pruner lives inside an agent
|
|
254
|
+
// preset's isolated group, so a top-level `ctx.get` can never find one and
|
|
255
|
+
// the queue route could only answer 503.
|
|
256
|
+
ctx.effect(
|
|
257
|
+
() => registerReviewPruner(this),
|
|
258
|
+
'contextCompressionSelector.reviewRegistry()',
|
|
259
|
+
)
|
|
260
|
+
|
|
244
261
|
// TokenPilot-inspired R4: upgrade the review queue to durable storage when
|
|
245
262
|
// the optional storageDomain seam is available; the memory fallback above
|
|
246
263
|
// serves every session until (and unless) that open succeeds.
|
|
@@ -325,6 +342,10 @@ export class ToolResultPruner extends Service {
|
|
|
325
342
|
// TokenPilot-inspired E1: advisory estimator pass, strictly off the
|
|
326
343
|
// synchronous chain. Verdicts only feed the next pressure pass.
|
|
327
344
|
void this.postflightEstimatorPass(agent.session, signal).catch(() => undefined)
|
|
345
|
+
// Advisory relevance advisor: statistics and suggestions only — its
|
|
346
|
+
// summaries, scores, and decay figure never touch any decision path.
|
|
347
|
+
// Strictly fire-and-forget, with its own backoff state.
|
|
348
|
+
void this.postflightAdvisorPass(agent.session, turn, signal).catch(() => undefined)
|
|
328
349
|
})
|
|
329
350
|
}
|
|
330
351
|
|
|
@@ -380,18 +401,16 @@ export class ToolResultPruner extends Service {
|
|
|
380
401
|
const planned = eligible
|
|
381
402
|
.map(candidate => this.planNative(candidate, session, stage, policy, view))
|
|
382
403
|
.filter((entry): entry is PlannedReplacement => entry !== null)
|
|
383
|
-
landed.push(...this.landAll(session, this.triageForReview(session, policy, planned)))
|
|
404
|
+
landed.push(...this.landAll(session, this.triageForReview(session, policy, planned, 'history')))
|
|
384
405
|
if (landed.length === 0) {
|
|
385
|
-
const
|
|
386
|
-
? [candidate.count.tokens] : [])
|
|
406
|
+
const chars = eligible.map(candidate => candidate.characterPressure)
|
|
387
407
|
this.auditComponent(session, policy, 'native-tool-result', 'pressure', 'skipped',
|
|
388
|
-
|
|
389
|
-
:
|
|
390
|
-
:
|
|
391
|
-
:
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
...(exact.length === 0 ? {} : { currentTokens: Math.max(...exact) }),
|
|
408
|
+
chars.length === 0 ? 'no-tool-result-candidates'
|
|
409
|
+
: Math.max(...chars) <= charsForTokens(policy.nativeTriggerTokens) ? 'at-or-below-trigger'
|
|
410
|
+
: planned.length === 0 ? 'no-valid-reduction'
|
|
411
|
+
: 'recovery-tool-unavailable', {
|
|
412
|
+
measurementKind: 'characters',
|
|
413
|
+
...(chars.length === 0 ? {} : { currentTokens: charsToTokens(Math.max(...chars)) }),
|
|
395
414
|
triggerTokens: policy.nativeTriggerTokens,
|
|
396
415
|
targetTokens: policy.nativeTargetTokens,
|
|
397
416
|
})
|
|
@@ -416,7 +435,7 @@ export class ToolResultPruner extends Service {
|
|
|
416
435
|
capacityPressure,
|
|
417
436
|
)
|
|
418
437
|
if (historyAllowed) {
|
|
419
|
-
landed.push(...this.landAll(session, this.triageForReview(session, policy, historyOutcome.plans)))
|
|
438
|
+
landed.push(...this.landAll(session, this.triageForReview(session, policy, historyOutcome.plans, 'history')))
|
|
420
439
|
}
|
|
421
440
|
}
|
|
422
441
|
} else {
|
|
@@ -424,7 +443,7 @@ export class ToolResultPruner extends Service {
|
|
|
424
443
|
if (historyAllowed) {
|
|
425
444
|
historyOutcome = this.planHistoricalAging(session, policy, view)
|
|
426
445
|
if (historyOutcome.kind === 'planned') {
|
|
427
|
-
landed.push(...this.landAll(session, this.triageForReview(session, policy, historyOutcome.plans)))
|
|
446
|
+
landed.push(...this.landAll(session, this.triageForReview(session, policy, historyOutcome.plans, 'history')))
|
|
428
447
|
}
|
|
429
448
|
}
|
|
430
449
|
}
|
|
@@ -587,8 +606,7 @@ export class ToolResultPruner extends Service {
|
|
|
587
606
|
for (const candidate of this.snapshot(session, measureForCompaction(this.ctx, session))) {
|
|
588
607
|
if (samples.length >= 3) break
|
|
589
608
|
if (candidate.event.data.turn === undefined) continue
|
|
590
|
-
|
|
591
|
-
if (tokens === undefined || tokens <= policy.freshTriggerTokens) continue
|
|
609
|
+
if (candidate.characterPressure <= charsForTokens(policy.freshTriggerTokens)) continue
|
|
592
610
|
const path = toolCallPath(candidate.call.arguments)
|
|
593
611
|
if (path === undefined) continue
|
|
594
612
|
if (isSupersededRead(events, candidate.seq, path)) continue
|
|
@@ -639,6 +657,106 @@ export class ToolResultPruner extends Service {
|
|
|
639
657
|
})
|
|
640
658
|
}
|
|
641
659
|
|
|
660
|
+
// ─────────── Advisory relevance advisor (statistics & suggestions only) ───────────
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Advisory advisor pass at the turn boundary, strictly fire-and-forget.
|
|
664
|
+
* Produces todolist-bound tail-task summaries, incremental relevance
|
|
665
|
+
* scores, and a prefix-decay figure — all observational. Every short
|
|
666
|
+
* circuit below (mode off, re-entry, cooldown, no task semantics, no
|
|
667
|
+
* direct endpoint) returns without touching any state the pruning chain
|
|
668
|
+
* reads, so the default configuration adds exactly zero behavior.
|
|
669
|
+
*/
|
|
670
|
+
private async postflightAdvisorPass(session: Session, turn: number, signal: AbortSignal): Promise<void> {
|
|
671
|
+
const policy = this.activePolicy(session)
|
|
672
|
+
const presetOptions = policy?.presetOptions
|
|
673
|
+
const advisor = presetOptions?.advisor
|
|
674
|
+
if (policy === undefined || presetOptions === undefined || advisor === undefined || advisor.mode === '') return
|
|
675
|
+
const advisorState = getAdvisorState(session)
|
|
676
|
+
if (advisorState.inFlight) return
|
|
677
|
+
if (isCoolingDown(advisorState.failures, Date.now())) return
|
|
678
|
+
|
|
679
|
+
const events = sessionEvents(session)
|
|
680
|
+
const task = collectTaskSemantics(events)
|
|
681
|
+
if (task === undefined) return
|
|
682
|
+
|
|
683
|
+
const settings = this.activeSettings(session).presetOptions ?? {}
|
|
684
|
+
if (advisor.mode === 'direct'
|
|
685
|
+
&& (settings.estimatorBaseUrl === undefined || settings.estimatorBaseUrl.length === 0
|
|
686
|
+
|| settings.estimatorModel === undefined || settings.estimatorModel.length === 0)) {
|
|
687
|
+
// The advisor reuses the estimator's direct endpoint; when it is not
|
|
688
|
+
// configured there is nothing to ask, so record the aligned reason and
|
|
689
|
+
// back off instead of re-emitting the audit at every turn.
|
|
690
|
+
emitCompressionAudit(this.ctx.logger, {
|
|
691
|
+
schemaVersion: 1,
|
|
692
|
+
kind: 'advisor-outcome',
|
|
693
|
+
sessionId: String(session.id),
|
|
694
|
+
phase: 'summary',
|
|
695
|
+
channel: 'direct',
|
|
696
|
+
ok: false,
|
|
697
|
+
turnIndex: turn,
|
|
698
|
+
reason: 'no-direct-endpoint',
|
|
699
|
+
latencyMs: 0,
|
|
700
|
+
})
|
|
701
|
+
advisorState.failures = {
|
|
702
|
+
failures: (advisorState.failures?.failures ?? 0) + 1,
|
|
703
|
+
cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1),
|
|
704
|
+
}
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let channel = this.state.advisorChannels.get(session)
|
|
709
|
+
if (channel === undefined) {
|
|
710
|
+
channel = new SideChannel(this.ctx, settings, {
|
|
711
|
+
mode: advisor.mode,
|
|
712
|
+
timeoutMs: advisor.timeoutMs,
|
|
713
|
+
maxTokens: 512,
|
|
714
|
+
})
|
|
715
|
+
this.state.advisorChannels.set(session, channel)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const view = measureForCompaction(this.ctx, session)
|
|
719
|
+
const candidates = this.snapshot(session, view)
|
|
720
|
+
.filter(candidate => !this.isRecoveryExempt(session, candidate))
|
|
721
|
+
.map(candidate => ({
|
|
722
|
+
seq: candidate.seq,
|
|
723
|
+
characterPressure: candidate.characterPressure,
|
|
724
|
+
preview: advisorCandidatePreview(candidate.call.name, candidate.event.data.message.content),
|
|
725
|
+
}))
|
|
726
|
+
|
|
727
|
+
let sawFailure = false
|
|
728
|
+
const outcome = await runSessionAdvisorPass(session, channel, record => {
|
|
729
|
+
if (record.ok === false) sawFailure = true
|
|
730
|
+
emitCompressionAudit(this.ctx.logger, record)
|
|
731
|
+
}, {
|
|
732
|
+
profile: policy.profile,
|
|
733
|
+
sessionId: String(session.id),
|
|
734
|
+
turn,
|
|
735
|
+
candidates,
|
|
736
|
+
task: {
|
|
737
|
+
source: task.source,
|
|
738
|
+
todoVersion: task.todoVersion,
|
|
739
|
+
taskText: task.taskText,
|
|
740
|
+
},
|
|
741
|
+
advisor: {
|
|
742
|
+
refreshTurns: advisor.refreshTurns,
|
|
743
|
+
scoreThreshold: advisor.scoreThreshold,
|
|
744
|
+
sampleLimit: advisor.sampleLimit,
|
|
745
|
+
minTokens: advisor.minTokens,
|
|
746
|
+
},
|
|
747
|
+
tailText: collectTailText(events),
|
|
748
|
+
signal,
|
|
749
|
+
})
|
|
750
|
+
if (outcome === undefined && sawFailure && signal.aborted === false) {
|
|
751
|
+
advisorState.failures = {
|
|
752
|
+
failures: (advisorState.failures?.failures ?? 0) + 1,
|
|
753
|
+
cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1),
|
|
754
|
+
}
|
|
755
|
+
} else if (outcome !== undefined) {
|
|
756
|
+
advisorState.failures = undefined
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
642
760
|
// ─────────── TokenPilot-inspired R4: human-gated review pipeline ───────────
|
|
643
761
|
|
|
644
762
|
/**
|
|
@@ -710,6 +828,7 @@ export class ToolResultPruner extends Service {
|
|
|
710
828
|
session: Session,
|
|
711
829
|
policy: CompressionPolicy,
|
|
712
830
|
plans: readonly PlannedReplacement[],
|
|
831
|
+
stage: 'fresh' | 'history' = 'history',
|
|
713
832
|
): readonly PlannedReplacement[] {
|
|
714
833
|
const queue = this.reviewQueueFor(session, policy)
|
|
715
834
|
if (queue === undefined || plans.length === 0) return plans
|
|
@@ -728,6 +847,10 @@ export class ToolResultPruner extends Service {
|
|
|
728
847
|
? {}
|
|
729
848
|
: { remainingTurns: this.state.estimatorRemainingTurns.get(session) },
|
|
730
849
|
estimatorSeqs,
|
|
850
|
+
// Fresh plans shape content before its first request — it is not in the
|
|
851
|
+
// KV cache yet, so no cache break occurs and the refill penalty would
|
|
852
|
+
// be a phantom cost pricing every realistic fresh batch into drop.
|
|
853
|
+
stage,
|
|
731
854
|
}
|
|
732
855
|
const classified = classifyCandidates(plans.map(plan => ({
|
|
733
856
|
sourceSeq: plan.sourceSeq,
|
|
@@ -773,7 +896,6 @@ export class ToolResultPruner extends Service {
|
|
|
773
896
|
try {
|
|
774
897
|
const view = measureForCompaction(this.ctx, session)
|
|
775
898
|
const candidatesBySeq = new Map(this.snapshot(session, view).map(candidate => [candidate.seq, candidate]))
|
|
776
|
-
const plans: PlannedReplacement[] = []
|
|
777
899
|
const settled: {
|
|
778
900
|
proposal: ReviewProposalRecord
|
|
779
901
|
auditItems?: ReviewProposalRecord['items']
|
|
@@ -1404,11 +1526,8 @@ export class ToolResultPruner extends Service {
|
|
|
1404
1526
|
const plans = new Map<number, PlannedReplacement>()
|
|
1405
1527
|
let freshPlanned = 0
|
|
1406
1528
|
const dedupeEnabled = policy.presetOptions?.dedupeToolResults === true
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1409
|
-
const maxCandidateTokens = exactAvailable
|
|
1410
|
-
? Math.max(...exactCandidateTokens as number[])
|
|
1411
|
-
: undefined
|
|
1529
|
+
const candidateChars = candidates.map(candidate => candidate.characterPressure)
|
|
1530
|
+
const maxCandidateChars = candidateChars.length === 0 ? undefined : Math.max(...candidateChars)
|
|
1412
1531
|
if (policy.freshEnabled) {
|
|
1413
1532
|
if (candidates.some(candidate => candidate.call.name !== 'context_compression_retrieve'
|
|
1414
1533
|
&& candidate.count.kind !== 'exact-tokenizer')) {
|
|
@@ -1430,37 +1549,33 @@ export class ToolResultPruner extends Service {
|
|
|
1430
1549
|
}
|
|
1431
1550
|
}
|
|
1432
1551
|
}
|
|
1433
|
-
let
|
|
1552
|
+
let aggregateInputChars: number | undefined
|
|
1434
1553
|
let aggregatePlanned = 0
|
|
1435
1554
|
if (policy.aggregateEnabled) {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
?? exactTokens(candidate.count) ?? 0), 0)
|
|
1441
|
-
: 0
|
|
1442
|
-
if (aggregateAvailable) aggregateInputTokens = total
|
|
1443
|
-
if (aggregateAvailable && total > policy.aggregateTriggerTokens) {
|
|
1555
|
+
let total = candidates.reduce((sum, candidate) => sum
|
|
1556
|
+
+ (plans.get(candidate.seq)?.charsAfter ?? candidate.characterPressure), 0)
|
|
1557
|
+
aggregateInputChars = total
|
|
1558
|
+
if (total > charsForTokens(policy.aggregateTriggerTokens)) {
|
|
1444
1559
|
const remaining = candidates
|
|
1445
1560
|
.filter(candidate => !this.isRecoveryExempt(session, candidate))
|
|
1446
1561
|
.sort((a, b) => Number(isError(a)) - Number(isError(b))
|
|
1447
|
-
|| (plans.get(b.seq)?.
|
|
1448
|
-
- (plans.get(a.seq)?.
|
|
1562
|
+
|| (plans.get(b.seq)?.charsAfter ?? b.characterPressure)
|
|
1563
|
+
- (plans.get(a.seq)?.charsAfter ?? a.characterPressure))
|
|
1449
1564
|
for (const candidate of remaining) {
|
|
1450
1565
|
const previous = plans.get(candidate.seq)
|
|
1451
1566
|
const plan = this.planAggregate(candidate, session, view)
|
|
1452
|
-
const
|
|
1453
|
-
if (plan === null || plan.
|
|
1567
|
+
const previousChars = previous?.charsAfter ?? candidate.characterPressure
|
|
1568
|
+
if (plan === null || plan.charsAfter >= previousChars) continue
|
|
1454
1569
|
plans.set(candidate.seq, plan)
|
|
1455
1570
|
aggregatePlanned += 1
|
|
1456
|
-
total -=
|
|
1457
|
-
if (total <= policy.aggregateTargetTokens) break
|
|
1571
|
+
total -= previousChars - plan.charsAfter
|
|
1572
|
+
if (total <= charsForTokens(policy.aggregateTargetTokens)) break
|
|
1458
1573
|
}
|
|
1459
|
-
if (total > policy.aggregateTargetTokens) {
|
|
1574
|
+
if (total > charsForTokens(policy.aggregateTargetTokens)) {
|
|
1460
1575
|
this.ctx.logger.warn(
|
|
1461
|
-
'context-compression fresh aggregate residual: %d
|
|
1576
|
+
'context-compression fresh aggregate residual: %d characters exceed target %d',
|
|
1462
1577
|
total,
|
|
1463
|
-
policy.aggregateTargetTokens,
|
|
1578
|
+
charsForTokens(policy.aggregateTargetTokens),
|
|
1464
1579
|
)
|
|
1465
1580
|
}
|
|
1466
1581
|
}
|
|
@@ -1469,7 +1584,7 @@ export class ToolResultPruner extends Service {
|
|
|
1469
1584
|
const freshCandidates = candidates
|
|
1470
1585
|
.map(candidate => plans.get(candidate.seq))
|
|
1471
1586
|
.filter((plan): plan is PlannedReplacement => plan !== undefined)
|
|
1472
|
-
const landed = this.landAll(session, this.triageForReview(session, policy, freshCandidates))
|
|
1587
|
+
const landed = this.landAll(session, this.triageForReview(session, policy, freshCandidates, 'fresh'))
|
|
1473
1588
|
const freshLanded = landed.some(entry => entry.stage === 'fresh'
|
|
1474
1589
|
&& plans.get(entry.originalSeq)?.component === 'fresh')
|
|
1475
1590
|
const aggregateLanded = landed.some(entry => entry.stage === 'fresh'
|
|
@@ -1478,13 +1593,12 @@ export class ToolResultPruner extends Service {
|
|
|
1478
1593
|
this.auditComponent(session, policy, 'fresh', 'fresh',
|
|
1479
1594
|
policy.freshEnabled ? 'skipped' : 'disabled',
|
|
1480
1595
|
!policy.freshEnabled ? 'profile-policy'
|
|
1481
|
-
:
|
|
1482
|
-
:
|
|
1483
|
-
: freshPlanned
|
|
1484
|
-
:
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
...(maxCandidateTokens === undefined ? {} : { currentTokens: maxCandidateTokens }),
|
|
1596
|
+
: (maxCandidateChars ?? 0) <= charsForTokens(policy.freshTriggerTokens) ? 'at-or-below-trigger'
|
|
1597
|
+
: freshPlanned > 0 && aggregatePlanned > 0 ? 'superseded-by-aggregate'
|
|
1598
|
+
: freshPlanned === 0 ? 'no-valid-reduction'
|
|
1599
|
+
: 'recovery-tool-unavailable', {
|
|
1600
|
+
measurementKind: 'characters',
|
|
1601
|
+
...(maxCandidateChars === undefined ? {} : { currentTokens: charsToTokens(maxCandidateChars) }),
|
|
1488
1602
|
triggerTokens: policy.freshTriggerTokens,
|
|
1489
1603
|
targetTokens: policy.freshTargetTokens,
|
|
1490
1604
|
})
|
|
@@ -1493,12 +1607,11 @@ export class ToolResultPruner extends Service {
|
|
|
1493
1607
|
this.auditComponent(session, policy, 'aggregate', 'fresh',
|
|
1494
1608
|
policy.aggregateEnabled ? 'skipped' : 'disabled',
|
|
1495
1609
|
!policy.aggregateEnabled ? 'profile-policy'
|
|
1496
|
-
:
|
|
1497
|
-
:
|
|
1498
|
-
:
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
...(aggregateInputTokens === undefined ? {} : { currentTokens: aggregateInputTokens }),
|
|
1610
|
+
: (aggregateInputChars ?? 0) <= charsForTokens(policy.aggregateTriggerTokens) ? 'at-or-below-trigger'
|
|
1611
|
+
: aggregatePlanned === 0 ? 'no-valid-reduction'
|
|
1612
|
+
: 'recovery-tool-unavailable', {
|
|
1613
|
+
measurementKind: 'characters',
|
|
1614
|
+
...(aggregateInputChars === undefined ? {} : { currentTokens: charsToTokens(aggregateInputChars) }),
|
|
1502
1615
|
triggerTokens: policy.aggregateTriggerTokens,
|
|
1503
1616
|
targetTokens: policy.aggregateTargetTokens,
|
|
1504
1617
|
})
|
|
@@ -1548,16 +1661,17 @@ export class ToolResultPruner extends Service {
|
|
|
1548
1661
|
view: CompactionTokenView,
|
|
1549
1662
|
): PlannedReplacement | null {
|
|
1550
1663
|
if (this.isRecoveryExempt(session, candidate)) return null
|
|
1551
|
-
|
|
1552
|
-
if (tokensBefore === undefined || tokensBefore <= policy.nativeTriggerTokens) return null
|
|
1664
|
+
if (candidate.characterPressure <= charsForTokens(policy.nativeTriggerTokens)) return null
|
|
1553
1665
|
const result = candidate.event.data.message.content[0]
|
|
1554
1666
|
if (onlyTextBlocks(result.content) === null) return null
|
|
1555
1667
|
const sourceSeq = rootToolResultSeq(session, candidate.seq)
|
|
1556
|
-
|
|
1668
|
+
// R9b site: the marker's retrieve hint starts at the event line right
|
|
1669
|
+
// after the retained head, computed inside nativePruneContent.
|
|
1670
|
+
const marker = (startLine: number): string => recoveryMarker(sourceRefFn(session, sourceSeq), 'tool result middle pruned', startLine)
|
|
1557
1671
|
let head = this.state.config.headChars
|
|
1558
1672
|
let tail = this.state.config.tailChars
|
|
1559
1673
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
1560
|
-
const threshold = head + codePointLength(marker) + tail
|
|
1674
|
+
const threshold = head + codePointLength(marker(1)) + tail
|
|
1561
1675
|
const content = nativePruneContent(result.content, threshold, head, tail, marker)
|
|
1562
1676
|
if (content !== null) {
|
|
1563
1677
|
const plan = this.plan(
|
|
@@ -1603,8 +1717,7 @@ export class ToolResultPruner extends Service {
|
|
|
1603
1717
|
const result = candidate.event.data.message.content[0]
|
|
1604
1718
|
const text = flattenPlainText(result.content)
|
|
1605
1719
|
if (text === undefined) return null
|
|
1606
|
-
|
|
1607
|
-
if (tokensBefore === undefined || tokensBefore <= policy.freshTriggerTokens) return null
|
|
1720
|
+
if (candidate.characterPressure <= charsForTokens(policy.freshTriggerTokens)) return null
|
|
1608
1721
|
let table = this.state.dedupeTables.get(session)
|
|
1609
1722
|
if (table === undefined) {
|
|
1610
1723
|
table = new DedupeTable()
|
|
@@ -1650,8 +1763,7 @@ export class ToolResultPruner extends Service {
|
|
|
1650
1763
|
// being reconsidered after their first request.
|
|
1651
1764
|
if (typeof candidate.event.surfaceOp === 'object') return null
|
|
1652
1765
|
const result = candidate.event.data.message.content[0]
|
|
1653
|
-
|
|
1654
|
-
if (tokensBefore === undefined || tokensBefore <= policy.freshTriggerTokens) return null
|
|
1766
|
+
if (candidate.characterPressure <= charsForTokens(policy.freshTriggerTokens)) return null
|
|
1655
1767
|
const sourceSeq = candidate.seq
|
|
1656
1768
|
const sourceRef = sourceRefFn(session, sourceSeq)
|
|
1657
1769
|
const textBlock = onlyTextBlock(result.content)
|
|
@@ -1678,9 +1790,12 @@ export class ToolResultPruner extends Service {
|
|
|
1678
1790
|
'fresh',
|
|
1679
1791
|
undefined,
|
|
1680
1792
|
view,
|
|
1681
|
-
{
|
|
1793
|
+
{
|
|
1794
|
+
noNetSavingsGuard: policy.presetOptions?.noNetSavingsGuard === true,
|
|
1795
|
+
...(output.elidedLines === undefined ? {} : { elidedLines: output.elidedLines }),
|
|
1796
|
+
},
|
|
1682
1797
|
)
|
|
1683
|
-
if (plan !== null && plan.
|
|
1798
|
+
if (plan !== null && plan.charsAfter <= charsForTokens(policy.freshTargetTokens)) return plan
|
|
1684
1799
|
}
|
|
1685
1800
|
if (budgetChars === 1) break
|
|
1686
1801
|
budgetChars = Math.max(1, Math.floor(budgetChars / 2))
|
|
@@ -1721,6 +1836,12 @@ export class ToolResultPruner extends Service {
|
|
|
1721
1836
|
}
|
|
1722
1837
|
const sourceSeq = rootToolResultSeq(session, candidate.seq)
|
|
1723
1838
|
const sourceRef = sourceRefFn(session, sourceSeq)
|
|
1839
|
+
// The placeholder below is a single text block, so a rich tool result (for
|
|
1840
|
+
// example one carrying an image) must never reach it. The character basis
|
|
1841
|
+
// no longer inherits the exact-tokenizer precondition that used to reject
|
|
1842
|
+
// this path implicitly, so the guard has to be explicit.
|
|
1843
|
+
const redacted = candidate.event.data.message.content[0]
|
|
1844
|
+
if (onlyTextBlocks(redacted.content) === null) return null
|
|
1724
1845
|
const text = [
|
|
1725
1846
|
'[Tool result reduced to satisfy the completed-step aggregate budget]',
|
|
1726
1847
|
`tool: ${candidate.call.name}`,
|
|
@@ -1798,17 +1919,9 @@ export class ToolResultPruner extends Service {
|
|
|
1798
1919
|
): HistoryPlanOutcome {
|
|
1799
1920
|
const candidates = this.snapshot(session, view)
|
|
1800
1921
|
const events = sessionEvents(session)
|
|
1801
|
-
const
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
if (tokens === undefined) {
|
|
1805
|
-
this.warnExactUnavailable(session, view, 'history')
|
|
1806
|
-
return { kind: 'exact-tokenizer-unavailable' }
|
|
1807
|
-
}
|
|
1808
|
-
exact.push(tokens)
|
|
1809
|
-
}
|
|
1810
|
-
const total = exact.reduce((sum, tokens) => sum + tokens, 0)
|
|
1811
|
-
const trigger = policy.historyTriggerTokens
|
|
1922
|
+
const chars = candidates.map(candidate => candidate.characterPressure)
|
|
1923
|
+
const total = chars.reduce((sum, charsOfNode) => sum + charsOfNode, 0)
|
|
1924
|
+
const trigger = charsForTokens(policy.historyTriggerTokens)
|
|
1812
1925
|
// Full-request last chance: ordinary prose, images, prompts, or schemas can
|
|
1813
1926
|
// push the complete request past the Auto Compact deadline before the tool
|
|
1814
1927
|
// results alone cross the profile trigger.
|
|
@@ -1835,19 +1948,18 @@ export class ToolResultPruner extends Service {
|
|
|
1835
1948
|
}
|
|
1836
1949
|
const planned: PlannedReplacement[] = []
|
|
1837
1950
|
let reclaim = 0
|
|
1838
|
-
//
|
|
1839
|
-
//
|
|
1840
|
-
//
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
const microTarget = deadline === undefined ? undefined : Math.max(0, deadline - policy.historyMinReclaimTokens)
|
|
1951
|
+
// Linked batches must reach the deadline target; unlinked batches keep
|
|
1952
|
+
// the traditional minimum-reclaim commit threshold. All arithmetic runs on
|
|
1953
|
+
// the character basis: token-named thresholds enter via charsForTokens.
|
|
1954
|
+
const minReclaimChars = charsForTokens(policy.historyMinReclaimTokens)
|
|
1955
|
+
const microTarget = deadline === undefined ? undefined : Math.max(0, charsForTokens(deadline) - minReclaimChars)
|
|
1844
1956
|
const required = Math.max(
|
|
1845
|
-
|
|
1957
|
+
minReclaimChars,
|
|
1846
1958
|
total - trigger,
|
|
1847
|
-
...(microTarget === undefined ? [] : [view.totalTokens - microTarget]),
|
|
1959
|
+
...(microTarget === undefined ? [] : [charsForTokens(view.totalTokens) - microTarget]),
|
|
1848
1960
|
)
|
|
1849
1961
|
const batchTarget = microTarget === undefined
|
|
1850
|
-
?
|
|
1962
|
+
? minReclaimChars
|
|
1851
1963
|
: required
|
|
1852
1964
|
for (const candidate of eligible) {
|
|
1853
1965
|
const result = candidate.event.data.message.content[0]
|
|
@@ -1872,7 +1984,7 @@ export class ToolResultPruner extends Service {
|
|
|
1872
1984
|
)
|
|
1873
1985
|
if (plan === null) continue
|
|
1874
1986
|
planned.push(plan)
|
|
1875
|
-
reclaim += plan.
|
|
1987
|
+
reclaim += plan.charsBefore - plan.charsAfter
|
|
1876
1988
|
if (reclaim >= required) break
|
|
1877
1989
|
continue
|
|
1878
1990
|
}
|
|
@@ -1891,7 +2003,7 @@ export class ToolResultPruner extends Service {
|
|
|
1891
2003
|
)
|
|
1892
2004
|
if (plan === null) continue
|
|
1893
2005
|
planned.push(plan)
|
|
1894
|
-
reclaim += plan.
|
|
2006
|
+
reclaim += plan.charsBefore - plan.charsAfter
|
|
1895
2007
|
if (reclaim >= required) break
|
|
1896
2008
|
continue
|
|
1897
2009
|
}
|
|
@@ -1931,10 +2043,11 @@ export class ToolResultPruner extends Service {
|
|
|
1931
2043
|
'history',
|
|
1932
2044
|
policy.historyMode,
|
|
1933
2045
|
view,
|
|
2046
|
+
{ ...(output.elidedLines === undefined ? {} : { elidedLines: output.elidedLines }) },
|
|
1934
2047
|
)
|
|
1935
2048
|
if (plan === null) continue
|
|
1936
2049
|
planned.push(plan)
|
|
1937
|
-
reclaim += plan.
|
|
2050
|
+
reclaim += plan.charsBefore - plan.charsAfter
|
|
1938
2051
|
if (reclaim >= required) break
|
|
1939
2052
|
}
|
|
1940
2053
|
// Linked batches must reach the deadline target; unlinked batches keep
|
|
@@ -1949,9 +2062,8 @@ export class ToolResultPruner extends Service {
|
|
|
1949
2062
|
session: Session,
|
|
1950
2063
|
policy: CompressionPolicy,
|
|
1951
2064
|
view: CompactionTokenView,
|
|
1952
|
-
): Set<number>
|
|
2065
|
+
): Set<number> {
|
|
1953
2066
|
const candidates = this.snapshot(session, view)
|
|
1954
|
-
if (candidates.some(candidate => exactTokens(candidate.count) === undefined)) return null
|
|
1955
2067
|
return this.protectedHistoryCandidateSeqs(candidates, policy)
|
|
1956
2068
|
}
|
|
1957
2069
|
|
|
@@ -1967,14 +2079,14 @@ export class ToolResultPruner extends Service {
|
|
|
1967
2079
|
const candidate = candidates[index]
|
|
1968
2080
|
if (candidate !== undefined) protectedSeqs.add(candidate.seq)
|
|
1969
2081
|
}
|
|
1970
|
-
let
|
|
2082
|
+
let recentChars = 0
|
|
1971
2083
|
for (let index = candidates.length - 1;
|
|
1972
|
-
index >= 0 &&
|
|
2084
|
+
index >= 0 && recentChars < charsForTokens(policy.historyKeepRecentTokens);
|
|
1973
2085
|
index--) {
|
|
1974
2086
|
const candidate = candidates[index]
|
|
1975
2087
|
if (candidate === undefined) continue
|
|
1976
2088
|
protectedSeqs.add(candidate.seq)
|
|
1977
|
-
|
|
2089
|
+
recentChars += candidate.characterPressure
|
|
1978
2090
|
}
|
|
1979
2091
|
return protectedSeqs
|
|
1980
2092
|
}
|
|
@@ -1988,26 +2100,22 @@ export class ToolResultPruner extends Service {
|
|
|
1988
2100
|
const tailTrim = policy.tailTrim
|
|
1989
2101
|
if (tailTrim?.enabled !== true) return
|
|
1990
2102
|
const events = sessionEvents(session)
|
|
1991
|
-
if (view.
|
|
1992
|
-
|| view.currentSurface.tokens <= tailTrim.triggerTokens) {
|
|
1993
|
-
if (view.currentSurface.kind !== 'exact-tokenizer') this.warnExactUnavailable(session, view, 'tailtrim')
|
|
2103
|
+
if (view.currentSurfaceChars <= charsForTokens(tailTrim.triggerTokens)) {
|
|
1994
2104
|
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
...(view.currentSurface.kind === 'exact-tokenizer'
|
|
1999
|
-
? { currentTokens: view.currentSurface.tokens }
|
|
2000
|
-
: {}),
|
|
2105
|
+
'at-or-below-trigger', {
|
|
2106
|
+
measurementKind: 'characters',
|
|
2107
|
+
currentTokens: charsToTokens(view.currentSurfaceChars),
|
|
2001
2108
|
triggerTokens: tailTrim.triggerTokens,
|
|
2002
2109
|
})
|
|
2003
2110
|
return
|
|
2004
2111
|
}
|
|
2005
2112
|
const surfaceCount = view.currentSurface
|
|
2113
|
+
const exactSurface = surfaceCount.kind === 'exact-tokenizer' ? surfaceCount : undefined
|
|
2006
2114
|
if (!this.hasRecoveryTool(session)) {
|
|
2007
2115
|
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
2008
2116
|
'recovery-tool-unavailable', {
|
|
2009
|
-
measurementKind: '
|
|
2010
|
-
currentTokens:
|
|
2117
|
+
measurementKind: 'characters',
|
|
2118
|
+
currentTokens: charsToTokens(view.currentSurfaceChars),
|
|
2011
2119
|
triggerTokens: tailTrim.triggerTokens,
|
|
2012
2120
|
})
|
|
2013
2121
|
return
|
|
@@ -2015,23 +2123,15 @@ export class ToolResultPruner extends Service {
|
|
|
2015
2123
|
if (!hasOpenTurn(session)) {
|
|
2016
2124
|
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
2017
2125
|
'no-open-turn', {
|
|
2018
|
-
measurementKind: '
|
|
2019
|
-
currentTokens:
|
|
2126
|
+
measurementKind: 'characters',
|
|
2127
|
+
currentTokens: charsToTokens(view.currentSurfaceChars),
|
|
2020
2128
|
triggerTokens: tailTrim.triggerTokens,
|
|
2021
2129
|
})
|
|
2022
2130
|
return
|
|
2023
2131
|
}
|
|
2024
2132
|
const protectedResults = this.protectedHistoryResultSeqs(session, policy, view)
|
|
2025
|
-
if (protectedResults === null) {
|
|
2026
|
-
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
2027
|
-
'exact-tokenizer-unavailable-in-protected-set', {
|
|
2028
|
-
measurementKind: 'unavailable',
|
|
2029
|
-
currentTokens: surfaceCount.tokens,
|
|
2030
|
-
triggerTokens: tailTrim.triggerTokens,
|
|
2031
|
-
})
|
|
2032
|
-
return
|
|
2033
|
-
}
|
|
2034
2133
|
const measured = new Map(view.measuredNodes.map(node => [node.seq, node.count]))
|
|
2134
|
+
const nodeChars = new Map(view.measuredNodes.map(node => [node.seq, node.characterPressure]))
|
|
2035
2135
|
const heuristic = new Map(view.nodes.map(node => [node.seq, node.tokens]))
|
|
2036
2136
|
const completedTurns = new Set<number>()
|
|
2037
2137
|
const completedSteps = new Set<string>()
|
|
@@ -2090,34 +2190,58 @@ export class ToolResultPruner extends Service {
|
|
|
2090
2190
|
if (roots.some(root => root === null)) continue
|
|
2091
2191
|
const sourceEventSeqs = roots as number[]
|
|
2092
2192
|
if (new Set(sourceEventSeqs).size !== sourceEventSeqs.length) continue
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2193
|
+
// Exact tokens stay telemetry-only: they are recorded when every
|
|
2194
|
+
// shadowed node shares the surface tokenizer identity, and derived from
|
|
2195
|
+
// characters otherwise. The skip decisions above and below are all
|
|
2196
|
+
// character-based.
|
|
2197
|
+
let exactTokensBefore: number | undefined
|
|
2198
|
+
if (exactSurface !== undefined) {
|
|
2199
|
+
let sum = 0
|
|
2200
|
+
let allExact = true
|
|
2201
|
+
for (const seq of shadowedSeqs) {
|
|
2202
|
+
const count = measured.get(seq)
|
|
2203
|
+
if (count?.kind !== 'exact-tokenizer'
|
|
2204
|
+
|| count.tokenizerId !== exactSurface.tokenizerId
|
|
2205
|
+
|| count.tokenizerRevision !== exactSurface.tokenizerRevision) {
|
|
2206
|
+
allExact = false
|
|
2207
|
+
break
|
|
2208
|
+
}
|
|
2209
|
+
sum += count.tokens
|
|
2210
|
+
}
|
|
2211
|
+
if (allExact) exactTokensBefore = sum
|
|
2212
|
+
}
|
|
2213
|
+
const charsBefore = shadowedSeqs.reduce((sum, seq) => sum + (nodeChars.get(seq) ?? 0), 0)
|
|
2099
2214
|
const manifestSeq = events.length
|
|
2100
2215
|
const ref = tailTrimRef(String(session.id), manifestSeq)
|
|
2101
2216
|
const stub = tailTrimStub(ref, calls.map(call => call.name), sourceEventSeqs)
|
|
2102
2217
|
if (stub === null) continue
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
)
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2218
|
+
const stubChars = codePointLength(stub)
|
|
2219
|
+
if (stubChars <= 0
|
|
2220
|
+
|| charsBefore - stubChars < charsForTokens(policy.historyMinReclaimTokens)) continue
|
|
2221
|
+
let exactTokensAfter: number | undefined
|
|
2222
|
+
if (exactTokensBefore !== undefined && exactSurface !== undefined) {
|
|
2223
|
+
const stubCount = countExactCanonicalTextFields(
|
|
2224
|
+
[stub],
|
|
2225
|
+
candidate => view.countCanonicalText(candidate),
|
|
2226
|
+
'TailTrim group stub',
|
|
2227
|
+
)
|
|
2228
|
+
if (stubCount.kind === 'exact-tokenizer'
|
|
2229
|
+
&& stubCount.tokenizerId === exactSurface.tokenizerId
|
|
2230
|
+
&& stubCount.tokenizerRevision === exactSurface.tokenizerRevision) {
|
|
2231
|
+
exactTokensAfter = stubCount.tokens
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const exact = exactTokensBefore !== undefined && exactTokensAfter !== undefined
|
|
2235
|
+
const tokensBefore = exactTokensBefore ?? charsToTokens(charsBefore)
|
|
2236
|
+
const tokensAfter = exactTokensAfter ?? charsToTokens(stubChars)
|
|
2113
2237
|
const heuristicTokens = shadowedSeqs.reduce((sum, seq) => sum + (heuristic.get(seq) ?? 0), 0)
|
|
2114
2238
|
const range = { start: SessionSeq(assistantSeq), end: SessionSeq(resultSeqs.at(-1) ?? assistantSeq) }
|
|
2115
2239
|
const surfaceRange = { op: 'replace' as const, startSeq: range.start, endSeq: range.end }
|
|
2116
2240
|
if (!this.reserveTailTrimBoundaryAttempt(session)) {
|
|
2117
2241
|
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
2118
2242
|
'already-attempted-at-request-boundary', {
|
|
2119
|
-
measurementKind: '
|
|
2120
|
-
currentTokens:
|
|
2243
|
+
measurementKind: 'characters',
|
|
2244
|
+
currentTokens: charsToTokens(view.currentSurfaceChars),
|
|
2121
2245
|
triggerTokens: tailTrim.triggerTokens,
|
|
2122
2246
|
})
|
|
2123
2247
|
return
|
|
@@ -2156,17 +2280,22 @@ export class ToolResultPruner extends Service {
|
|
|
2156
2280
|
replacementSeq: replacement.seq,
|
|
2157
2281
|
sourceSeqs: sourceEventSeqs,
|
|
2158
2282
|
tokensBefore,
|
|
2159
|
-
tokensAfter
|
|
2160
|
-
tokensRemoved: tokensBefore -
|
|
2161
|
-
|
|
2162
|
-
|
|
2283
|
+
tokensAfter,
|
|
2284
|
+
tokensRemoved: tokensBefore - tokensAfter,
|
|
2285
|
+
measurementBasis: exact ? 'exact-tokenizer' : 'characters',
|
|
2286
|
+
tokenizerId: exact === true && exactSurface !== undefined
|
|
2287
|
+
? exactSurface.tokenizerId
|
|
2288
|
+
: 'characters',
|
|
2289
|
+
tokenizerRevision: exact === true && exactSurface !== undefined
|
|
2290
|
+
? exactSurface.tokenizerRevision
|
|
2291
|
+
: 'chars-per-token-4.0',
|
|
2163
2292
|
})
|
|
2164
2293
|
return
|
|
2165
2294
|
}
|
|
2166
2295
|
this.auditComponent(session, policy, 'tail-trim', 'pressure', 'skipped',
|
|
2167
2296
|
'no-safe-eligible-tool-group', {
|
|
2168
|
-
measurementKind: '
|
|
2169
|
-
currentTokens:
|
|
2297
|
+
measurementKind: 'characters',
|
|
2298
|
+
currentTokens: charsToTokens(view.currentSurfaceChars),
|
|
2170
2299
|
triggerTokens: tailTrim.triggerTokens,
|
|
2171
2300
|
})
|
|
2172
2301
|
}
|
|
@@ -2211,17 +2340,23 @@ export class ToolResultPruner extends Service {
|
|
|
2211
2340
|
component: CompressionAuditComponent,
|
|
2212
2341
|
historyMode: HistoryMode | undefined,
|
|
2213
2342
|
view: CompactionTokenView,
|
|
2214
|
-
options: { readonly noNetSavingsGuard?: boolean } = {},
|
|
2343
|
+
options: { readonly noNetSavingsGuard?: boolean, readonly elidedLines?: number } = {},
|
|
2215
2344
|
): PlannedReplacement | null {
|
|
2345
|
+
const charsBefore = candidate.characterPressure
|
|
2346
|
+
const charsAfter = pressureCost(content)
|
|
2347
|
+
// Character proof replaces the exact-tokenizer precondition: a reduction
|
|
2348
|
+
// must shrink the decision surface regardless of the routed model id.
|
|
2349
|
+
if (charsAfter <= 0 || charsAfter >= charsBefore) return null
|
|
2350
|
+
// Tokens are telemetry only: exact when both sides share one bundled
|
|
2351
|
+
// tokenizer identity, otherwise derived from the character measurement.
|
|
2216
2352
|
const countBefore = candidate.count
|
|
2217
|
-
if (countBefore.kind !== 'exact-tokenizer') return null
|
|
2218
2353
|
const countAfter = countToolContent(content, view)
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2354
|
+
const exact = countBefore.kind === 'exact-tokenizer'
|
|
2355
|
+
&& countAfter.kind === 'exact-tokenizer'
|
|
2356
|
+
&& countAfter.tokenizerId === countBefore.tokenizerId
|
|
2357
|
+
&& countAfter.tokenizerRevision === countBefore.tokenizerRevision
|
|
2358
|
+
const tokensBefore = exact ? countBefore.tokens : charsToTokens(charsBefore)
|
|
2359
|
+
const tokensAfter = exact ? countAfter.tokens : charsToTokens(charsAfter)
|
|
2225
2360
|
// TokenPilot-style no-net-savings: even when the exact tokenizer reports a
|
|
2226
2361
|
// saving, a replacement whose text is not smaller than its original adds
|
|
2227
2362
|
// noise without reclaiming context. Text-level because the placeholder
|
|
@@ -2235,8 +2370,6 @@ export class ToolResultPruner extends Service {
|
|
|
2235
2370
|
if (replacementChars >= originalChars) return null
|
|
2236
2371
|
}
|
|
2237
2372
|
}
|
|
2238
|
-
const charsBefore = candidate.characterPressure
|
|
2239
|
-
const charsAfter = pressureCost(content)
|
|
2240
2373
|
return {
|
|
2241
2374
|
candidate,
|
|
2242
2375
|
content,
|
|
@@ -2249,8 +2382,10 @@ export class ToolResultPruner extends Service {
|
|
|
2249
2382
|
charsAfter,
|
|
2250
2383
|
tokensBefore,
|
|
2251
2384
|
tokensAfter,
|
|
2252
|
-
|
|
2253
|
-
|
|
2385
|
+
measurementBasis: exact ? 'exact-tokenizer' : 'characters',
|
|
2386
|
+
tokenizerId: exact ? countBefore.tokenizerId : 'characters',
|
|
2387
|
+
tokenizerRevision: exact ? countBefore.tokenizerRevision : 'chars-per-token-4.0',
|
|
2388
|
+
...options.elidedLines === undefined ? {} : { elidedLines: options.elidedLines },
|
|
2254
2389
|
}
|
|
2255
2390
|
}
|
|
2256
2391
|
|
|
@@ -2301,8 +2436,12 @@ export class ToolResultPruner extends Service {
|
|
|
2301
2436
|
tokensBefore: plan.tokensBefore,
|
|
2302
2437
|
tokensAfter: plan.tokensAfter,
|
|
2303
2438
|
tokensRemoved: plan.tokensBefore - plan.tokensAfter,
|
|
2439
|
+
measurementBasis: plan.measurementBasis,
|
|
2304
2440
|
tokenizerId: plan.tokenizerId,
|
|
2305
2441
|
tokenizerRevision: plan.tokenizerRevision,
|
|
2442
|
+
// task_4c/G7 telemetry: original-event lines the reducer elided. Audit
|
|
2443
|
+
// record ONLY — the replacement content is untouched by this field.
|
|
2444
|
+
...plan.elidedLines === undefined ? {} : { elidedLines: plan.elidedLines },
|
|
2306
2445
|
})
|
|
2307
2446
|
// R4: the four-state summary counts automatic-path rewrites at the single
|
|
2308
2447
|
// landing chokepoint; the review-approved batch settles its own counters
|
|
@@ -2380,7 +2519,7 @@ export class ToolResultPruner extends Service {
|
|
|
2380
2519
|
policy.historyMode === 'capacity-pressure'
|
|
2381
2520
|
? 'below-micro-deadline' : 'adaptive-cost-rejected', {
|
|
2382
2521
|
historyMode: policy.historyMode,
|
|
2383
|
-
measurementKind:
|
|
2522
|
+
measurementKind: 'characters',
|
|
2384
2523
|
currentTokens: view.totalTokens,
|
|
2385
2524
|
...(capacityTrigger === undefined ? {} : { triggerTokens: capacityTrigger }),
|
|
2386
2525
|
})
|
|
@@ -2390,14 +2529,14 @@ export class ToolResultPruner extends Service {
|
|
|
2390
2529
|
const lastChance = deadline !== undefined && view.totalTokens >= deadline
|
|
2391
2530
|
const detail = (extra: Readonly<Record<string, number>> = {}): Readonly<{
|
|
2392
2531
|
historyMode?: HistoryMode
|
|
2393
|
-
measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'unavailable'
|
|
2532
|
+
measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'characters' | 'unavailable'
|
|
2394
2533
|
currentTokens?: number
|
|
2395
2534
|
triggerTokens?: number
|
|
2396
2535
|
reclaimTokens?: number
|
|
2397
2536
|
requiredTokens?: number
|
|
2398
2537
|
}> => ({
|
|
2399
2538
|
historyMode: policy.historyMode,
|
|
2400
|
-
measurementKind:
|
|
2539
|
+
measurementKind: 'characters',
|
|
2401
2540
|
currentTokens: view.totalTokens,
|
|
2402
2541
|
...(outcome.kind === 'insufficient-reclaim' || outcome.kind === 'cannot-reach-deadline-target'
|
|
2403
2542
|
? { reclaimTokens: outcome.reclaim, requiredTokens: outcome.required }
|
|
@@ -2405,10 +2544,6 @@ export class ToolResultPruner extends Service {
|
|
|
2405
2544
|
...extra,
|
|
2406
2545
|
})
|
|
2407
2546
|
switch (outcome.kind) {
|
|
2408
|
-
case 'exact-tokenizer-unavailable':
|
|
2409
|
-
this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
|
|
2410
|
-
'exact-tokenizer-unavailable', detail({ triggerTokens: policy.historyTriggerTokens }))
|
|
2411
|
-
return
|
|
2412
2547
|
case 'below-profile-trigger':
|
|
2413
2548
|
this.auditComponent(session, policy, 'history', 'pressure', 'skipped',
|
|
2414
2549
|
'below-profile-trigger', detail({ triggerTokens: policy.historyTriggerTokens }))
|
|
@@ -2454,7 +2589,7 @@ export class ToolResultPruner extends Service {
|
|
|
2454
2589
|
reason: string,
|
|
2455
2590
|
detail: Readonly<{
|
|
2456
2591
|
historyMode?: HistoryMode
|
|
2457
|
-
measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'unavailable'
|
|
2592
|
+
measurementKind?: 'exact-tokenizer' | 'tokenizer-estimate' | 'characters' | 'unavailable'
|
|
2458
2593
|
currentTokens?: number
|
|
2459
2594
|
triggerTokens?: number
|
|
2460
2595
|
targetTokens?: number
|
|
@@ -2567,7 +2702,7 @@ export class ToolResultPruner extends Service {
|
|
|
2567
2702
|
this.warnOnce(
|
|
2568
2703
|
session,
|
|
2569
2704
|
`exact-tokenizer:${gate}:${provider}\0${model}`,
|
|
2570
|
-
'context-compression %s
|
|
2705
|
+
'context-compression %s is measuring on the character basis: exact tokenizer counts are unavailable for %s/%s',
|
|
2571
2706
|
gate,
|
|
2572
2707
|
provider,
|
|
2573
2708
|
model,
|