dsh-context-compression-improved 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/CHANGELOG.ja.md +144 -83
- package/CHANGELOG.ko.md +143 -82
- package/CHANGELOG.md +278 -212
- package/CHANGELOG.zh.md +131 -77
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +16 -234
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +0 -101
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +0 -38
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +24 -271
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +2 -25
- package/packages/selector/src/pruner.ts +75 -403
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +45 -14
- package/scripts/toolclass-corpus-replay.mjs +281 -281
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
tailTrimStub,
|
|
33
33
|
} from './runtime/tail-trim.ts'
|
|
34
34
|
import { installContextCompressionRetrieve } from './runtime/retrieve.ts'
|
|
35
|
-
import type { PrunerState
|
|
35
|
+
import type { PrunerState } from './pruner/state.ts'
|
|
36
36
|
import { countOmittedLines, CAPACITY_PRESSURE_RATIO } from './pruner/tuning.ts'
|
|
37
37
|
import type { ToolCallInfo, SnapshotCandidate, PlannedReplacement, HistoryPlanOutcome } from './pruner/types.ts'
|
|
38
38
|
import {
|
|
@@ -70,16 +70,10 @@ import {
|
|
|
70
70
|
type EstimatorSample,
|
|
71
71
|
} from './runtime/tokenpilot/estimator.ts'
|
|
72
72
|
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
ReviewQueue,
|
|
78
|
-
type ReviewProposalRecord,
|
|
79
|
-
type ReviewReceipt,
|
|
80
|
-
} from './runtime/tokenpilot/review-queue.ts'
|
|
81
|
-
import { registerReviewPruner, sharedReviewStore } from './runtime/tokenpilot/review-registry.ts'
|
|
82
|
-
import { openReviewStorage } from './runtime/tokenpilot/review-storage.ts'
|
|
73
|
+
adviseCandidates,
|
|
74
|
+
DEFAULT_ADVICE_ALPHA,
|
|
75
|
+
DEFAULT_ADVICE_HIGH_IMPACT_TOKENS,
|
|
76
|
+
} from './runtime/tokenpilot/benefit.ts'
|
|
83
77
|
import { SideChannel } from './runtime/tokenpilot/sidechannel.ts'
|
|
84
78
|
import {
|
|
85
79
|
advisorCandidatePreview,
|
|
@@ -241,34 +235,11 @@ export class ToolResultPruner extends Service {
|
|
|
241
235
|
activeRequestBoundaries: new WeakMap(),
|
|
242
236
|
tailTrimBoundaryAttempts: new WeakMap(),
|
|
243
237
|
policyResolutionAudits: new WeakMap(),
|
|
244
|
-
|
|
245
|
-
reviewQueues: new WeakMap(),
|
|
246
|
-
reviewClocks: new WeakMap(),
|
|
238
|
+
turnClocks: new WeakMap(),
|
|
247
239
|
estimatorRemainingTurns: new WeakMap(),
|
|
248
240
|
advisorChannels: new WeakMap(),
|
|
249
|
-
reviewSummaries: new WeakMap(),
|
|
250
241
|
}
|
|
251
242
|
|
|
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
|
-
|
|
261
|
-
// TokenPilot-inspired R4: upgrade the review queue to durable storage when
|
|
262
|
-
// the optional storageDomain seam is available; the memory fallback above
|
|
263
|
-
// serves every session until (and unless) that open succeeds.
|
|
264
|
-
void openReviewStorage(name => this.ctx.get(name as never))
|
|
265
|
-
.then(store => {
|
|
266
|
-
if (store !== undefined) this.state.reviewStore = store
|
|
267
|
-
})
|
|
268
|
-
.catch(() => {
|
|
269
|
-
this.ctx.logger.warn('context-compression review storage unavailable; keeping in-memory review queue')
|
|
270
|
-
})
|
|
271
|
-
|
|
272
243
|
ctx.on('session/event', (session, event) => {
|
|
273
244
|
this.scanForSeededNativeSummary(session)
|
|
274
245
|
if (event.type === 'compaction/summary') {
|
|
@@ -300,7 +271,7 @@ export class ToolResultPruner extends Service {
|
|
|
300
271
|
// Only the immediately preceding step can contain results that have
|
|
301
272
|
// not yet been exposed. This freezes both REDUCE and KEEP decisions:
|
|
302
273
|
// older original events are never reconsidered after a profile change.
|
|
303
|
-
this.
|
|
274
|
+
this.turnClock(agent.session, turn)
|
|
304
275
|
this.runRequestBoundary(agent.session, turn, step - 1, signal)
|
|
305
276
|
} catch (error: unknown) {
|
|
306
277
|
this.auditFailure(agent.session, 'fresh', 'request-boundary', error)
|
|
@@ -329,16 +300,6 @@ export class ToolResultPruner extends Service {
|
|
|
329
300
|
this.auditFailure(agent.session, 'fresh', 'terminal-pass', error)
|
|
330
301
|
ctx.logger.warn('context-compression terminal pass failed open: %o', error)
|
|
331
302
|
}
|
|
332
|
-
// TokenPilot-inspired R4: review-pipeline housekeeping at the turn
|
|
333
|
-
// boundary, strictly fail-open — expire stale pendings, then execute
|
|
334
|
-
// every approved proposal as one merged batch. Order matters: expiring
|
|
335
|
-
// first keeps just-expired proposals from executing.
|
|
336
|
-
try {
|
|
337
|
-
this.expireReviewProposals(agent.session, turn)
|
|
338
|
-
this.applyApprovedProposals(agent.session)
|
|
339
|
-
} catch (error: unknown) {
|
|
340
|
-
ctx.logger.warn('context-compression review turn-boundary pass failed open: %o', error)
|
|
341
|
-
}
|
|
342
303
|
// TokenPilot-inspired E1: advisory estimator pass, strictly off the
|
|
343
304
|
// synchronous chain. Verdicts only feed the next pressure pass.
|
|
344
305
|
void this.postflightEstimatorPass(agent.session, signal).catch(() => undefined)
|
|
@@ -401,7 +362,7 @@ export class ToolResultPruner extends Service {
|
|
|
401
362
|
const planned = eligible
|
|
402
363
|
.map(candidate => this.planNative(candidate, session, stage, policy, view))
|
|
403
364
|
.filter((entry): entry is PlannedReplacement => entry !== null)
|
|
404
|
-
landed.push(...this.landAll(session, this.
|
|
365
|
+
landed.push(...this.landAll(session, this.adviseReplacements(session, policy, planned, 'history')))
|
|
405
366
|
if (landed.length === 0) {
|
|
406
367
|
const chars = eligible.map(candidate => candidate.characterPressure)
|
|
407
368
|
this.auditComponent(session, policy, 'native-tool-result', 'pressure', 'skipped',
|
|
@@ -435,7 +396,7 @@ export class ToolResultPruner extends Service {
|
|
|
435
396
|
capacityPressure,
|
|
436
397
|
)
|
|
437
398
|
if (historyAllowed) {
|
|
438
|
-
landed.push(...this.landAll(session, this.
|
|
399
|
+
landed.push(...this.landAll(session, this.adviseReplacements(session, policy, historyOutcome.plans, 'history')))
|
|
439
400
|
}
|
|
440
401
|
}
|
|
441
402
|
} else {
|
|
@@ -443,7 +404,7 @@ export class ToolResultPruner extends Service {
|
|
|
443
404
|
if (historyAllowed) {
|
|
444
405
|
historyOutcome = this.planHistoricalAging(session, policy, view)
|
|
445
406
|
if (historyOutcome.kind === 'planned') {
|
|
446
|
-
landed.push(...this.landAll(session, this.
|
|
407
|
+
landed.push(...this.landAll(session, this.adviseReplacements(session, policy, historyOutcome.plans, 'history')))
|
|
447
408
|
}
|
|
448
409
|
}
|
|
449
410
|
}
|
|
@@ -757,369 +718,85 @@ export class ToolResultPruner extends Service {
|
|
|
757
718
|
}
|
|
758
719
|
}
|
|
759
720
|
|
|
760
|
-
// ───────────
|
|
761
|
-
|
|
762
|
-
/**
|
|
763
|
-
* The per-session review queue, or `undefined` while review mode is off
|
|
764
|
-
* (every review path must then behave exactly like before).
|
|
765
|
-
*/
|
|
766
|
-
private reviewQueueFor(session: Session, policy: CompressionPolicy | undefined): ReviewQueue | undefined {
|
|
767
|
-
const presetOptions = policy?.presetOptions
|
|
768
|
-
if (presetOptions?.reviewMode !== true) return undefined
|
|
769
|
-
let queue = this.state.reviewQueues.get(session)
|
|
770
|
-
if (queue === undefined) {
|
|
771
|
-
queue = new ReviewQueue(this.state.reviewStore, { timeoutTurns: presetOptions.reviewTimeoutTurns })
|
|
772
|
-
this.state.reviewQueues.set(session, queue)
|
|
773
|
-
}
|
|
774
|
-
return queue
|
|
775
|
-
}
|
|
721
|
+
// ─────────── Advisory benefit model (statistics & suggestions only) ───────────
|
|
776
722
|
|
|
777
723
|
/**
|
|
778
|
-
* Monotonic per-session turn clock for
|
|
779
|
-
*
|
|
780
|
-
*
|
|
724
|
+
* Monotonic per-session turn clock for advisory records. Bumped by the agent
|
|
725
|
+
* loop payloads (`pre-step`); passes without a turn coordinate reuse the last
|
|
726
|
+
* observed value.
|
|
781
727
|
*/
|
|
782
|
-
private
|
|
783
|
-
const previous = this.state.
|
|
728
|
+
private turnClock(session: Session, turn?: number): number {
|
|
729
|
+
const previous = this.state.turnClocks.get(session) ?? 0
|
|
784
730
|
const next = typeof turn === 'number' && Number.isSafeInteger(turn) && turn > previous ? turn : previous
|
|
785
|
-
this.state.
|
|
731
|
+
this.state.turnClocks.set(session, next)
|
|
786
732
|
return next
|
|
787
733
|
}
|
|
788
734
|
|
|
789
|
-
private auditReviewOutcome(
|
|
790
|
-
session: Session,
|
|
791
|
-
proposal: Pick<ReviewProposalRecord, 'id' | 'kind' | 'items'>,
|
|
792
|
-
event: 'enqueue' | 'expire' | 'decide' | 'apply-void' | 'apply-receipt',
|
|
793
|
-
extra: {
|
|
794
|
-
decision?: 'approved' | 'rejected' | 'ignored' | undefined
|
|
795
|
-
receiptStatus?: 'applied' | 'deferred' | undefined
|
|
796
|
-
reasonCode?: string | undefined
|
|
797
|
-
} = {},
|
|
798
|
-
turnIndex?: number,
|
|
799
|
-
): void {
|
|
800
|
-
const tokensBefore = proposal.items.reduce((sum, item) => sum + item.tokensBefore, 0)
|
|
801
|
-
const tokensAfter = proposal.items.reduce((sum, item) => sum + item.tokensAfter, 0)
|
|
802
|
-
emitCompressionAudit(this.ctx.logger, {
|
|
803
|
-
schemaVersion: 1,
|
|
804
|
-
kind: 'review-outcome',
|
|
805
|
-
sessionId: String(session.id),
|
|
806
|
-
proposalId: proposal.id,
|
|
807
|
-
proposalKind: proposal.kind,
|
|
808
|
-
event,
|
|
809
|
-
...extra.decision === undefined ? {} : { decision: extra.decision },
|
|
810
|
-
...extra.receiptStatus === undefined ? {} : { receiptStatus: extra.receiptStatus },
|
|
811
|
-
...extra.reasonCode === undefined ? {} : { reasonCode: extra.reasonCode },
|
|
812
|
-
itemSeqs: proposal.items.map(item => item.seq),
|
|
813
|
-
tokensBefore,
|
|
814
|
-
tokensAfter,
|
|
815
|
-
...turnIndex === undefined ? {} : { turnIndex },
|
|
816
|
-
})
|
|
817
|
-
}
|
|
818
|
-
|
|
819
735
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
736
|
+
* Advisory benefit-model hook — what the retired human-gated review pipeline
|
|
737
|
+
* left behind. It is the IDENTITY on the landing path: every plan it is given
|
|
738
|
+
* comes back unchanged, because a reduction must never block automatic
|
|
739
|
+
* processing. The model's band is published as a `reduction-advice` audit and
|
|
740
|
+
* snapshotted onto the advisor state for the read-only report route, so the
|
|
741
|
+
* cache-accounting insight survives without a gate.
|
|
826
742
|
*/
|
|
827
|
-
private
|
|
743
|
+
private adviseReplacements(
|
|
828
744
|
session: Session,
|
|
829
745
|
policy: CompressionPolicy,
|
|
830
746
|
plans: readonly PlannedReplacement[],
|
|
831
747
|
stage: 'fresh' | 'history' = 'history',
|
|
832
748
|
): readonly PlannedReplacement[] {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
.
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
alpha: presetOptions.cacheHitDiscountAlpha,
|
|
749
|
+
if (plans.length === 0) return plans
|
|
750
|
+
const remainingTurns = this.state.estimatorRemainingTurns.get(session)
|
|
751
|
+
const advice = adviseCandidates(plans.map(plan => ({
|
|
752
|
+
sourceSeq: plan.sourceSeq,
|
|
753
|
+
tokensBefore: plan.tokensBefore,
|
|
754
|
+
tokensAfter: plan.tokensAfter,
|
|
755
|
+
})), {
|
|
756
|
+
alpha: DEFAULT_ADVICE_ALPHA,
|
|
842
757
|
// One-phase approximation of the tail that a mutation must refill: the
|
|
843
758
|
// frozen protected recent-token tail (findings.md, 约束与依赖).
|
|
844
759
|
tailTokens: Math.max(1, policy.historyKeepRecentTokens),
|
|
845
|
-
|
|
846
|
-
...
|
|
847
|
-
? {}
|
|
848
|
-
: { remainingTurns: this.state.estimatorRemainingTurns.get(session) },
|
|
849
|
-
estimatorSeqs,
|
|
760
|
+
highImpactTokens: DEFAULT_ADVICE_HIGH_IMPACT_TOKENS,
|
|
761
|
+
...remainingTurns === undefined ? {} : { remainingTurns },
|
|
850
762
|
// 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
|
-
//
|
|
763
|
+
// KV cache yet, so no cache break occurs and the refill penalty would be
|
|
764
|
+
// a phantom cost pricing every realistic fresh batch into the
|
|
765
|
+
// not-worth-it band.
|
|
853
766
|
stage,
|
|
767
|
+
})
|
|
768
|
+
if (advice === undefined) return plans
|
|
769
|
+
const turn = this.turnClock(session)
|
|
770
|
+
const itemSeqs = plans.map(plan => plan.sourceSeq)
|
|
771
|
+
// Observational snapshot only: the advisory-only invariant (K13) holds
|
|
772
|
+
// because no decision path reads this field.
|
|
773
|
+
getAdvisorState(session).lastAdvice = {
|
|
774
|
+
band: advice.band,
|
|
775
|
+
turn,
|
|
776
|
+
itemSeqs,
|
|
777
|
+
recoveredTokens: advice.benefit.recoveredTokens,
|
|
778
|
+
penaltyTokens: advice.benefit.penaltyTokens,
|
|
779
|
+
...advice.benefit.paybackTurns === undefined ? {} : { paybackTurns: advice.benefit.paybackTurns },
|
|
854
780
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}
|
|
875
|
-
return plans.filter(plan => autoSeqs.has(plan.sourceSeq))
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
/**
|
|
879
|
-
* Execute every approved proposal of one session as ONE merged replacement
|
|
880
|
-
* batch at the current turn boundary, following the upstream applied-receipt
|
|
881
|
-
* discipline: applied receipts are built only from real mutation evidence —
|
|
882
|
-
* estimates never cross into applied savings.
|
|
883
|
-
*
|
|
884
|
-
* Per proposal: every item's frozen digest is re-checked against the current
|
|
885
|
-
* surface content; any mismatch voids the whole proposal (deferred with a
|
|
886
|
-
* reason code) instead of deleting something the user never approved.
|
|
887
|
-
* Fail-open: any unexpected error only logs and leaves the queue intact.
|
|
888
|
-
*/
|
|
889
|
-
applyApprovedProposals(session: Session): void {
|
|
890
|
-
const policy = this.activePolicy(session)
|
|
891
|
-
const queue = this.reviewQueueFor(session, policy)
|
|
892
|
-
if (queue === undefined) return
|
|
893
|
-
const sessionId = String(session.id)
|
|
894
|
-
const approved = [...queue.listApproved(sessionId)]
|
|
895
|
-
if (approved.length === 0) return
|
|
896
|
-
try {
|
|
897
|
-
const view = measureForCompaction(this.ctx, session)
|
|
898
|
-
const candidatesBySeq = new Map(this.snapshot(session, view).map(candidate => [candidate.seq, candidate]))
|
|
899
|
-
const settled: {
|
|
900
|
-
proposal: ReviewProposalRecord
|
|
901
|
-
auditItems?: ReviewProposalRecord['items']
|
|
902
|
-
receipt: ReviewReceipt
|
|
903
|
-
}[] = []
|
|
904
|
-
const now = new Date().toISOString()
|
|
905
|
-
for (const proposal of approved) {
|
|
906
|
-
// Digest re-check at the execution point (the approval point cannot
|
|
907
|
-
// protect against later mutations of the same seq).
|
|
908
|
-
const voidedReason = this.reviewProposalVoided(session, proposal)
|
|
909
|
-
if (voidedReason !== undefined) {
|
|
910
|
-
settled.push({
|
|
911
|
-
proposal,
|
|
912
|
-
receipt: {
|
|
913
|
-
status: 'deferred',
|
|
914
|
-
reasonCode: voidedReason,
|
|
915
|
-
estimatedTokens: proposal.benefit.recoveredTokens,
|
|
916
|
-
updatedAt: now,
|
|
917
|
-
},
|
|
918
|
-
})
|
|
919
|
-
continue
|
|
920
|
-
}
|
|
921
|
-
// One merged batch: every still-valid proposal takes the same
|
|
922
|
-
// whole-result placeholder path in a single landAll call, so the tail
|
|
923
|
-
// refill penalty is paid once for the whole approval set.
|
|
924
|
-
const batchPlans: PlannedReplacement[] = []
|
|
925
|
-
let planned = true
|
|
926
|
-
for (const item of proposal.items) {
|
|
927
|
-
const candidate = candidatesBySeq.get(item.seq)
|
|
928
|
-
if (candidate === undefined) {
|
|
929
|
-
planned = false
|
|
930
|
-
break
|
|
931
|
-
}
|
|
932
|
-
const plan = this.planAggregate(
|
|
933
|
-
candidate,
|
|
934
|
-
session,
|
|
935
|
-
view,
|
|
936
|
-
'review-approved-whole-result',
|
|
937
|
-
'pressure',
|
|
938
|
-
undefined,
|
|
939
|
-
'history',
|
|
940
|
-
policy?.historyMode,
|
|
941
|
-
)
|
|
942
|
-
if (plan === null) {
|
|
943
|
-
planned = false
|
|
944
|
-
break
|
|
945
|
-
}
|
|
946
|
-
batchPlans.push(plan)
|
|
947
|
-
}
|
|
948
|
-
if (!planned || batchPlans.length === 0) {
|
|
949
|
-
settled.push({
|
|
950
|
-
proposal,
|
|
951
|
-
receipt: {
|
|
952
|
-
status: 'deferred',
|
|
953
|
-
reasonCode: 'review_receipt_execution_invalid',
|
|
954
|
-
estimatedTokens: proposal.benefit.recoveredTokens,
|
|
955
|
-
updatedAt: now,
|
|
956
|
-
},
|
|
957
|
-
})
|
|
958
|
-
continue
|
|
959
|
-
}
|
|
960
|
-
const landed = this.landAll(session, batchPlans)
|
|
961
|
-
if (landed.length === 0) {
|
|
962
|
-
settled.push({
|
|
963
|
-
proposal,
|
|
964
|
-
receipt: {
|
|
965
|
-
status: 'deferred',
|
|
966
|
-
reasonCode: 'review_receipt_execution_invalid',
|
|
967
|
-
estimatedTokens: proposal.benefit.recoveredTokens,
|
|
968
|
-
updatedAt: now,
|
|
969
|
-
},
|
|
970
|
-
})
|
|
971
|
-
continue
|
|
972
|
-
}
|
|
973
|
-
const landedForProposal = new Map(landed.map(entry => [entry.originalSeq, entry]))
|
|
974
|
-
const measuredItems = proposal.items.map((item) => {
|
|
975
|
-
const entry = landedForProposal.get(item.seq)
|
|
976
|
-
return entry === undefined ? item : {
|
|
977
|
-
...item,
|
|
978
|
-
tokensBefore: entry.tokensBefore,
|
|
979
|
-
tokensAfter: entry.tokensAfter,
|
|
980
|
-
}
|
|
981
|
-
})
|
|
982
|
-
const appliedTokens = measuredItems.reduce(
|
|
983
|
-
(sum, item) => sum + item.tokensBefore - item.tokensAfter,
|
|
984
|
-
0,
|
|
985
|
-
)
|
|
986
|
-
settled.push({
|
|
987
|
-
proposal,
|
|
988
|
-
// The audit face carries the MEASURED numbers of the executed
|
|
989
|
-
// mutation; estimates never cross into applied savings.
|
|
990
|
-
auditItems: measuredItems,
|
|
991
|
-
receipt: {
|
|
992
|
-
status: 'applied',
|
|
993
|
-
estimatedTokens: proposal.benefit.recoveredTokens,
|
|
994
|
-
appliedTokens,
|
|
995
|
-
updatedAt: now,
|
|
996
|
-
},
|
|
997
|
-
})
|
|
998
|
-
}
|
|
999
|
-
for (const { proposal, auditItems, receipt } of settled) {
|
|
1000
|
-
queue.recordReceipt(sessionId, proposal.id, receipt)
|
|
1001
|
-
const summary = this.reviewSummaryFor(session)
|
|
1002
|
-
if (receipt.status === 'applied') summary.reviewApplied += 1
|
|
1003
|
-
else summary.voided += 1
|
|
1004
|
-
this.auditReviewOutcome(
|
|
1005
|
-
session,
|
|
1006
|
-
auditItems === undefined
|
|
1007
|
-
? proposal
|
|
1008
|
-
: { ...proposal, items: auditItems },
|
|
1009
|
-
receipt.status === 'applied' ? 'apply-receipt' : 'apply-void',
|
|
1010
|
-
receipt.status === 'applied'
|
|
1011
|
-
? { receiptStatus: 'applied' }
|
|
1012
|
-
: { receiptStatus: 'deferred', reasonCode: receipt.reasonCode },
|
|
1013
|
-
)
|
|
1014
|
-
}
|
|
1015
|
-
} catch (error: unknown) {
|
|
1016
|
-
// Fail-open: a broken apply must never break the turn or the queue.
|
|
1017
|
-
this.ctx.logger.warn('context-compression review apply failed open: %o', error)
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/**
|
|
1022
|
-
* Current surface content at one seq: the newest covering replacement's
|
|
1023
|
-
* blocks when the seq was rewritten, otherwise the original event's blocks.
|
|
1024
|
-
*/
|
|
1025
|
-
private surfaceContentAt(session: Session, seq: number): ContentBlock[] | undefined {
|
|
1026
|
-
let content: ContentBlock[] | undefined
|
|
1027
|
-
for (const event of sessionEvents(session)) {
|
|
1028
|
-
if (event.type !== 'tool/result') continue
|
|
1029
|
-
const op = event.surfaceOp
|
|
1030
|
-
if (typeof op === 'object' && op.op === 'replace' && op.startSeq <= seq && seq <= op.endSeq) {
|
|
1031
|
-
content = event.data.message.content[0].content
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
if (content !== undefined) return content
|
|
1035
|
-
const original = sessionEvents(session).find(entry => entry.seq === seq)
|
|
1036
|
-
return original?.type === 'tool/result' ? original.data.message.content[0].content : undefined
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* The execution-point digest check: `undefined` when every item's frozen
|
|
1041
|
-
* digest still matches the current surface content, otherwise the aligned
|
|
1042
|
-
* reason code explaining the void.
|
|
1043
|
-
*/
|
|
1044
|
-
private reviewProposalVoided(
|
|
1045
|
-
session: Session,
|
|
1046
|
-
proposal: ReviewProposalRecord,
|
|
1047
|
-
): 'review_receipt_digest_invalid' | 'review_receipt_missing_candidate' | undefined {
|
|
1048
|
-
for (const item of proposal.items) {
|
|
1049
|
-
const current = this.surfaceContentAt(session, item.seq)
|
|
1050
|
-
if (current === undefined) return 'review_receipt_missing_candidate'
|
|
1051
|
-
if (contentDigest(current) !== item.digest) return 'review_receipt_digest_invalid'
|
|
1052
|
-
}
|
|
1053
|
-
return undefined
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
private reviewSummaryFor(session: Session): ReviewSessionSummary {
|
|
1057
|
-
let summary = this.state.reviewSummaries.get(session)
|
|
1058
|
-
if (summary === undefined) {
|
|
1059
|
-
summary = { autoApplied: 0, reviewApplied: 0, expired: 0, voided: 0 }
|
|
1060
|
-
this.state.reviewSummaries.set(session, summary)
|
|
1061
|
-
}
|
|
1062
|
-
return summary
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
/** Live pending review proposals of one session; empty when review mode is off. */
|
|
1066
|
-
listReviewProposals(session: Session): readonly ReviewProposalRecord[] {
|
|
1067
|
-
const queue = this.reviewQueueFor(session, this.activePolicy(session))
|
|
1068
|
-
return queue?.listPending(String(session.id)) ?? []
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
/**
|
|
1072
|
-
* Every session's live pending proposals, for the floating window's
|
|
1073
|
-
* aggregate badge (the client carries no session id of its own).
|
|
1074
|
-
*/
|
|
1075
|
-
listAllReviewProposals(): readonly { readonly sessionId: string, readonly proposals: readonly ReviewProposalRecord[] }[] {
|
|
1076
|
-
const ids = this.state.reviewStore.ids?.() ?? []
|
|
1077
|
-
const reader = new ReviewQueue(this.state.reviewStore, { timeoutTurns: 1 })
|
|
1078
|
-
return ids
|
|
1079
|
-
.map(sessionId => ({ sessionId, proposals: [...reader.listPending(sessionId)] }))
|
|
1080
|
-
.filter(entry => entry.proposals.length > 0)
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
/** Four-state outcome counters of one session (floating-window summary row). */
|
|
1084
|
-
reviewSummary(session: Session): ReviewSessionSummary {
|
|
1085
|
-
return { ...this.reviewSummaryFor(session) }
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Record one human decision. Returns the outcome, or `undefined` when
|
|
1090
|
-
* review mode is off for this session (the route maps that to 503).
|
|
1091
|
-
*/
|
|
1092
|
-
decideReviewProposal(
|
|
1093
|
-
session: Session,
|
|
1094
|
-
proposalId: string,
|
|
1095
|
-
decision: 'approved' | 'rejected' | 'ignored',
|
|
1096
|
-
): { ok: true } | { ok: false, reason: 'unknown-proposal' | 'not-pending' } | undefined {
|
|
1097
|
-
const queue = this.reviewQueueFor(session, this.activePolicy(session))
|
|
1098
|
-
if (queue === undefined) return undefined
|
|
1099
|
-
const sessionId = String(session.id)
|
|
1100
|
-
const pending = queue.listPending(sessionId).find(entry => entry.id === proposalId)
|
|
1101
|
-
const outcome = queue.decide(sessionId, proposalId, decision)
|
|
1102
|
-
if (outcome.ok && pending !== undefined) {
|
|
1103
|
-
this.auditReviewOutcome(session, pending, 'decide', { decision }, this.reviewClock(session))
|
|
1104
|
-
}
|
|
1105
|
-
return outcome
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
/**
|
|
1109
|
-
* Expire stale pending proposals at one turn boundary and audit each.
|
|
1110
|
-
* Public because tests drive it directly; the turn-stopping handler calls
|
|
1111
|
-
* it with the loop's own turn index.
|
|
1112
|
-
*/
|
|
1113
|
-
expireReviewProposals(session: Session, turnIndex?: number): readonly ReviewProposalRecord[] {
|
|
1114
|
-
const queue = this.reviewQueueFor(session, this.activePolicy(session))
|
|
1115
|
-
if (queue === undefined) return []
|
|
1116
|
-
const clock = this.reviewClock(session, turnIndex)
|
|
1117
|
-
const expired = queue.expireTurn(String(session.id), clock)
|
|
1118
|
-
if (expired.length > 0) this.reviewSummaryFor(session).expired += expired.length
|
|
1119
|
-
for (const proposal of expired) {
|
|
1120
|
-
this.auditReviewOutcome(session, proposal, 'expire', {}, clock)
|
|
1121
|
-
}
|
|
1122
|
-
return expired
|
|
781
|
+
emitCompressionAudit(this.ctx.logger, {
|
|
782
|
+
schemaVersion: 1,
|
|
783
|
+
kind: 'reduction-advice',
|
|
784
|
+
sessionId: String(session.id),
|
|
785
|
+
profile: policy.profile,
|
|
786
|
+
band: advice.band,
|
|
787
|
+
stage,
|
|
788
|
+
itemSeqs,
|
|
789
|
+
pricedCandidates: advice.priced,
|
|
790
|
+
maxTokensBefore: advice.maxTokensBefore,
|
|
791
|
+
tokensBefore: plans.reduce((sum, plan) => sum + plan.tokensBefore, 0),
|
|
792
|
+
tokensAfter: plans.reduce((sum, plan) => sum + plan.tokensAfter, 0),
|
|
793
|
+
recoveredTokens: advice.benefit.recoveredTokens,
|
|
794
|
+
penaltyTokens: advice.benefit.penaltyTokens,
|
|
795
|
+
...advice.benefit.paybackTurns === undefined ? {} : { paybackTurns: advice.benefit.paybackTurns },
|
|
796
|
+
...advice.benefit.expectedSaving === undefined ? {} : { expectedSaving: advice.benefit.expectedSaving },
|
|
797
|
+
turnIndex: turn,
|
|
798
|
+
})
|
|
799
|
+
return plans
|
|
1123
800
|
}
|
|
1124
801
|
|
|
1125
802
|
private activePolicy(
|
|
@@ -1130,10 +807,11 @@ export class ToolResultPruner extends Service {
|
|
|
1130
807
|
const settings = this.activeSettings(session)
|
|
1131
808
|
try {
|
|
1132
809
|
// R4 bridge: the persisted settings document's presetOptions (the
|
|
1133
|
-
// settings-card writes, including
|
|
1134
|
-
// before this bridge only the estimator endpoint read them
|
|
1135
|
-
// every policy consumer saw the deployment defaults. User
|
|
1136
|
-
// over deployment config; absent fields inherit via
|
|
810
|
+
// settings-card writes, including the advisor keys) must reach the
|
|
811
|
+
// policy — before this bridge only the estimator endpoint read them
|
|
812
|
+
// directly and every policy consumer saw the deployment defaults. User
|
|
813
|
+
// settings win over deployment config; absent fields inherit via
|
|
814
|
+
// mergePresetOptions.
|
|
1137
815
|
const policy = resolvePolicy(
|
|
1138
816
|
settings.presetOptions === undefined
|
|
1139
817
|
? this.state.config
|
|
@@ -1584,7 +1262,7 @@ export class ToolResultPruner extends Service {
|
|
|
1584
1262
|
const freshCandidates = candidates
|
|
1585
1263
|
.map(candidate => plans.get(candidate.seq))
|
|
1586
1264
|
.filter((plan): plan is PlannedReplacement => plan !== undefined)
|
|
1587
|
-
const landed = this.landAll(session, this.
|
|
1265
|
+
const landed = this.landAll(session, this.adviseReplacements(session, policy, freshCandidates, 'fresh'))
|
|
1588
1266
|
const freshLanded = landed.some(entry => entry.stage === 'fresh'
|
|
1589
1267
|
&& plans.get(entry.originalSeq)?.component === 'fresh')
|
|
1590
1268
|
const aggregateLanded = landed.some(entry => entry.stage === 'fresh'
|
|
@@ -2443,12 +2121,6 @@ export class ToolResultPruner extends Service {
|
|
|
2443
2121
|
// record ONLY — the replacement content is untouched by this field.
|
|
2444
2122
|
...plan.elidedLines === undefined ? {} : { elidedLines: plan.elidedLines },
|
|
2445
2123
|
})
|
|
2446
|
-
// R4: the four-state summary counts automatic-path rewrites at the single
|
|
2447
|
-
// landing chokepoint; the review-approved batch settles its own counters
|
|
2448
|
-
// in applyApprovedProposals.
|
|
2449
|
-
if (plan.reducer !== 'review-approved-whole-result') {
|
|
2450
|
-
this.reviewSummaryFor(session).autoApplied += 1
|
|
2451
|
-
}
|
|
2452
2124
|
return {
|
|
2453
2125
|
originalSeq: candidate.seq,
|
|
2454
2126
|
sourceSeq: plan.sourceSeq,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Structured, content-free runtime audit records for context compression. */
|
|
2
2
|
|
|
3
|
+
import type { AdviceBand } from './tokenpilot/benefit.ts'
|
|
3
4
|
import type {
|
|
4
5
|
CompressionPolicy,
|
|
5
6
|
CompressionProfile,
|
|
@@ -204,29 +205,34 @@ export interface AdvisorOutcomeAuditRecord extends CompressionAuditBase {
|
|
|
204
205
|
readonly latencyMs: number
|
|
205
206
|
}
|
|
206
207
|
|
|
207
|
-
/**
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
readonly
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
readonly decision?: 'approved' | 'rejected' | 'ignored'
|
|
222
|
-
/** Execution receipt state (apply-receipt only). */
|
|
223
|
-
readonly receiptStatus?: 'applied' | 'deferred'
|
|
224
|
-
/** Aligned reason code (deferred receipts and void applications only). */
|
|
225
|
-
readonly reasonCode?: string
|
|
208
|
+
/**
|
|
209
|
+
* One advisory benefit-model label for a batch that LANDED. The retired review
|
|
210
|
+
* gate used these bands to withhold a batch; a reduction must never block
|
|
211
|
+
* automatic processing, so the band is published as advice instead. Numeric and
|
|
212
|
+
* enum fields only — never content.
|
|
213
|
+
*/
|
|
214
|
+
export interface ReductionAdviceAuditRecord extends CompressionAuditBase {
|
|
215
|
+
readonly kind: 'reduction-advice'
|
|
216
|
+
/** Profile the advised pass ran under. */
|
|
217
|
+
readonly profile: CompressionProfile
|
|
218
|
+
/** Band the benefit model labelled the landed batch with. */
|
|
219
|
+
readonly band: AdviceBand
|
|
220
|
+
/** Landing stage the batch was priced at. */
|
|
221
|
+
readonly stage: 'fresh' | 'history'
|
|
226
222
|
readonly itemSeqs: readonly number[]
|
|
223
|
+
/** Candidates that carried a positive recovery and were priced. */
|
|
224
|
+
readonly pricedCandidates: number
|
|
225
|
+
/** Largest single-candidate token mass in the batch. */
|
|
226
|
+
readonly maxTokensBefore: number
|
|
227
227
|
readonly tokensBefore: number
|
|
228
228
|
readonly tokensAfter: number
|
|
229
|
-
|
|
229
|
+
readonly recoveredTokens: number
|
|
230
|
+
readonly penaltyTokens: number
|
|
231
|
+
/** Turns of discounted recovery needed to recoup the refill penalty. */
|
|
232
|
+
readonly paybackTurns?: number
|
|
233
|
+
/** Discounted net benefit over the remaining session; omitted when Ŝ is unknown. */
|
|
234
|
+
readonly expectedSaving?: number
|
|
235
|
+
/** Turn index the batch landed at. */
|
|
230
236
|
readonly turnIndex?: number
|
|
231
237
|
}
|
|
232
238
|
|
|
@@ -241,7 +247,7 @@ export type CompressionAuditRecord =
|
|
|
241
247
|
| SummaryLocatorAuditRecord
|
|
242
248
|
| EstimatorOutcomeAuditRecord
|
|
243
249
|
| AdvisorOutcomeAuditRecord
|
|
244
|
-
|
|
|
250
|
+
| ReductionAdviceAuditRecord
|
|
245
251
|
|
|
246
252
|
/** Minimal logger method consumed by the audit publisher. */
|
|
247
253
|
export interface CompressionAuditLogger {
|