dsh-context-compression-improved 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.ja.md +51 -0
  2. package/CHANGELOG.ko.md +51 -0
  3. package/CHANGELOG.md +55 -0
  4. package/CHANGELOG.zh.md +45 -0
  5. package/package.json +1 -1
  6. package/packages/selector/lib/advisor-state.js +4 -231
  7. package/packages/selector/lib/client.d.ts +0 -24
  8. package/packages/selector/lib/client.js +6 -501
  9. package/packages/selector/lib/index.d.ts +4 -10
  10. package/packages/selector/lib/index.js +65 -235
  11. package/packages/selector/lib/pruner.d.ts +13 -248
  12. package/packages/selector/lib/pruner.js +148 -552
  13. package/packages/selector/src/client/EstimatorControls.tsx +277 -378
  14. package/packages/selector/src/client/index.ts +0 -17
  15. package/packages/selector/src/client/locales.ts +196 -234
  16. package/packages/selector/src/client/preset-options.ts +3 -2
  17. package/packages/selector/src/client/settings-section.tsx +8 -17
  18. package/packages/selector/src/index.ts +463 -710
  19. package/packages/selector/src/preset-overlay.ts +60 -1
  20. package/packages/selector/src/profiles.ts +4 -27
  21. package/packages/selector/src/pruner/state.ts +50 -73
  22. package/packages/selector/src/pruner.ts +2402 -2730
  23. package/packages/selector/src/runtime/audit.ts +27 -21
  24. package/packages/selector/src/runtime/config.ts +6 -32
  25. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +16 -0
  26. package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
  27. package/packages/selector/src/runtime/types.ts +0 -17
  28. package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
  29. package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
  30. package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
  31. package/packages/selector/tests/runtime/audit.spec.ts +35 -21
  32. package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
  33. package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
  34. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
  35. package/packages/selector/tests/settings-seat.client.spec.ts +16 -10
  36. package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
  37. package/scripts/packed-components-smoke.mjs +30 -8
  38. package/scripts/packed-install-e2e.mjs +69 -15
  39. package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
  40. package/packages/selector/src/client/review-scope.ts +0 -16
  41. package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
  42. package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
  43. package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
  44. package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
  45. package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
  46. package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
  47. package/packages/selector/tests/review-routes.host.spec.ts +0 -290
  48. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
  49. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
  50. package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
@@ -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
- /** Lifecycle of one human-gated review proposal. Only numeric and enum fields — never content. */
208
- export interface ReviewOutcomeAuditRecord extends CompressionAuditBase {
209
- readonly kind: 'review-outcome'
210
- /** Stable proposal id (sha-256 digest cut, 12 hex chars). */
211
- readonly proposalId: string
212
- /** Reduction kind the proposal came from. */
213
- readonly proposalKind: 'estimator' | 'dedup' | 'read-state'
214
- readonly event:
215
- | 'enqueue'
216
- | 'expire'
217
- | 'decide'
218
- | 'apply-void'
219
- | 'apply-receipt'
220
- /** Human decision (decide events only). */
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
- /** Turn index the event happened at. */
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
- | ReviewOutcomeAuditRecord
250
+ | ReductionAdviceAuditRecord
245
251
 
246
252
  /** Minimal logger method consumed by the audit publisher. */
247
253
  export interface CompressionAuditLogger {
@@ -127,6 +127,11 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
127
127
  const allowed = new Set([
128
128
  'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
129
129
  'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
130
+ // Legacy keys of the retired human-gated review pipeline. They stay in the
131
+ // ACCEPTED set on purpose: the keys are persisted in existing settings
132
+ // documents, and rejecting them would make this plugin fail to load on an
133
+ // upgrade. They are read by nothing — the gate they configured no longer
134
+ // exists, and a reduction never blocks automatic processing.
130
135
  'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
131
136
  'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
132
137
  'advisorMinTokens',
@@ -135,7 +140,7 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
135
140
  if (unknown !== undefined) {
136
141
  throw new TypeError(`Context-compression presetOptions: unknown key "${unknown}"`)
137
142
  }
138
- const booleans = ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'reviewMode'] as const
143
+ const booleans = ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'] as const
139
144
  for (const key of booleans) {
140
145
  const entry = value[key]
141
146
  if (entry !== undefined && typeof entry !== 'boolean') {
@@ -185,23 +190,6 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
185
190
  || estimatorTimeoutMs < 100 || estimatorTimeoutMs > 60_000)) {
186
191
  throw new TypeError('Context-compression presetOptions.estimatorTimeoutMs must be an integer between 100 and 60000')
187
192
  }
188
- const reviewTimeoutTurns = value.reviewTimeoutTurns
189
- if (reviewTimeoutTurns !== undefined
190
- && (typeof reviewTimeoutTurns !== 'number' || !Number.isSafeInteger(reviewTimeoutTurns) || reviewTimeoutTurns < 1)) {
191
- throw new TypeError('Context-compression presetOptions.reviewTimeoutTurns must be an integer of at least 1')
192
- }
193
- const cacheHitDiscountAlpha = value.cacheHitDiscountAlpha
194
- if (cacheHitDiscountAlpha !== undefined
195
- && (typeof cacheHitDiscountAlpha !== 'number' || !Number.isFinite(cacheHitDiscountAlpha)
196
- || cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) {
197
- throw new TypeError('Context-compression presetOptions.cacheHitDiscountAlpha must be a number strictly between 0 and 1')
198
- }
199
- const reviewHighImpactTokens = value.reviewHighImpactTokens
200
- if (reviewHighImpactTokens !== undefined
201
- && (typeof reviewHighImpactTokens !== 'number' || !Number.isSafeInteger(reviewHighImpactTokens)
202
- || reviewHighImpactTokens < 0)) {
203
- throw new TypeError('Context-compression presetOptions.reviewHighImpactTokens must be a non-negative integer')
204
- }
205
193
  for (const key of ['estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey'] as const) {
206
194
  const entry = value[key]
207
195
  if (entry !== undefined && typeof entry !== 'string') {
@@ -221,10 +209,6 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
221
209
  if (value.estimatorBaseUrl !== undefined) result.estimatorBaseUrl = value.estimatorBaseUrl as string
222
210
  if (value.estimatorApiKey !== undefined) result.estimatorApiKey = value.estimatorApiKey as string
223
211
  if (estimatorTimeoutMs !== undefined) result.estimatorTimeoutMs = estimatorTimeoutMs as number
224
- if (value.reviewMode !== undefined) result.reviewMode = value.reviewMode as boolean
225
- if (reviewTimeoutTurns !== undefined) result.reviewTimeoutTurns = reviewTimeoutTurns as number
226
- if (cacheHitDiscountAlpha !== undefined) result.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
227
- if (reviewHighImpactTokens !== undefined) result.reviewHighImpactTokens = reviewHighImpactTokens as number
228
212
  if (advisorMode !== undefined) result.advisorMode = advisorMode as '' | 'host' | 'direct'
229
213
  if (advisorTimeoutMs !== undefined) result.advisorTimeoutMs = advisorTimeoutMs as number
230
214
  if (advisorRefreshTurns !== undefined) result.advisorRefreshTurns = advisorRefreshTurns as number
@@ -498,12 +482,6 @@ const PRESET_OPTION_DEFAULTS: PresetOptions = deepFreeze({
498
482
  prefixStabilizer: true,
499
483
  readState: true,
500
484
  estimator: { mode: '' },
501
- // Review pipeline (beta) ships off: pending proposals never block the
502
- // automatic path until the user opts in.
503
- reviewMode: false,
504
- reviewTimeoutTurns: 6,
505
- cacheHitDiscountAlpha: 0.1,
506
- reviewHighImpactTokens: 4000,
507
485
  // Advisory advisor ships off: statistics and suggestions only, never a gate.
508
486
  advisor: {
509
487
  mode: '',
@@ -530,10 +508,6 @@ function mergePresetOptions(overrides: PresetOptionsSettings | undefined): Prese
530
508
  prefixStabilizer: overrides.prefixStabilizer ?? PRESET_OPTION_DEFAULTS.prefixStabilizer,
531
509
  readState: overrides.readState ?? PRESET_OPTION_DEFAULTS.readState,
532
510
  estimator: { mode: overrides.estimatorMode ?? PRESET_OPTION_DEFAULTS.estimator.mode },
533
- reviewMode: overrides.reviewMode ?? PRESET_OPTION_DEFAULTS.reviewMode,
534
- reviewTimeoutTurns: overrides.reviewTimeoutTurns ?? PRESET_OPTION_DEFAULTS.reviewTimeoutTurns,
535
- cacheHitDiscountAlpha: overrides.cacheHitDiscountAlpha ?? PRESET_OPTION_DEFAULTS.cacheHitDiscountAlpha,
536
- reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens,
537
511
  advisor: {
538
512
  mode: overrides.advisorMode ?? PRESET_OPTION_DEFAULTS.advisor.mode,
539
513
  timeoutMs: overrides.advisorTimeoutMs ?? PRESET_OPTION_DEFAULTS.advisor.timeoutMs,
@@ -13,6 +13,19 @@
13
13
  * plain import rather than constructor injection.
14
14
  */
15
15
  import type { Session } from '@deepseek-ai/dsh-session'
16
+ import type { AdviceBand } from './benefit.ts'
17
+
18
+ /** The most recent benefit-model label of a landed batch. Observational only:
19
+ * no decision path reads it — the report route serves it verbatim. */
20
+ export interface AdvisorAdviceSnapshot {
21
+ readonly band: AdviceBand
22
+ /** Turn index the advised batch landed at. */
23
+ readonly turn: number
24
+ readonly itemSeqs: readonly number[]
25
+ readonly recoveredTokens: number
26
+ readonly penaltyTokens: number
27
+ readonly paybackTurns?: number
28
+ }
16
29
 
17
30
  /** One relevance score for one surface seq, with the turn it was scored at. */
18
31
  export interface AdvisorScoreEntry {
@@ -59,6 +72,8 @@ export interface AdvisorState {
59
72
  inFlight: boolean
60
73
  /** Most recent decay computation, shared verbatim by audits and the report route. */
61
74
  lastDecay: { readonly decay: number, readonly weightedChars: number, readonly turn: number } | undefined
75
+ /** Most recent benefit-model advice; replaced by every advised batch. */
76
+ lastAdvice: AdvisorAdviceSnapshot | undefined
62
77
  }
63
78
 
64
79
  /** Upper bound of the per-session scores LRU. */
@@ -85,6 +100,7 @@ export function getAdvisorState(session: Session): AdvisorState {
85
100
  failures: undefined,
86
101
  inFlight: false,
87
102
  lastDecay: undefined,
103
+ lastAdvice: undefined,
88
104
  }
89
105
  advisorStates.set(session, state)
90
106
  }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * TokenPilot-inspired benefit model — **advisory only**.
3
+ *
4
+ * This module prices one pass's planned replacements as ONE merged mutation and
5
+ * labels the batch with the band the model would have chosen. The label is
6
+ * ADVICE: nothing here suppresses, delays, or rewrites a reduction. The former
7
+ * human-gated review pipeline consumed these bands to withhold the batch from
8
+ * landing; that gate was retired because a reduction must never block automatic
9
+ * processing. The bands now feed the `reduction-advice` audit and the advisor
10
+ * report, and every planned replacement that the rule engine produced lands.
11
+ *
12
+ * Cache accounting (TokenPilot paper): one merged mutation pays a one-time tail
13
+ * KV-cache refill penalty of `(1−α)·tailTokens`, and every later turn recovers
14
+ * the reclaimed tokens at the cache-hit discount `α`:
15
+ *
16
+ * ```
17
+ * R = Σ(tokensBefore − tokensAfter) // net reclaimed tokens
18
+ * paybackTurns = (1−α)·tailTokens / (α·R) // one-time refill / per-turn saving
19
+ * expectedSaving = α·R·max(0, Ŝ − paybackTurns) // Ŝ = estimated remaining turns
20
+ * ```
21
+ *
22
+ * The refill penalty only models mutations of already-cached context. A
23
+ * fresh-stage batch (shaped before its first request) is exempt via
24
+ * `refillPenaltyExempt`: payback is 0 and every reclaimed token saves from
25
+ * the very first turn. Without that exemption every realistic fresh batch
26
+ * prices into the `not-worth-it` band, which makes the advice useless exactly
27
+ * where it is most often consulted.
28
+ *
29
+ * `expectedSaving` is only produced when Ŝ is known (the estimator answered
30
+ * with `expectedRemainingTurns`); it is never fabricated from a guess.
31
+ *
32
+ * Pure functions only: the classifier needs no I/O, no session state, and no
33
+ * host services, so every label is unit-testable and audit-replayable.
34
+ */
35
+
36
+ /** The minimal per-candidate face the benefit model consumes. */
37
+ export interface BenefitCandidate {
38
+ readonly sourceSeq: number
39
+ readonly tokensBefore: number
40
+ readonly tokensAfter: number
41
+ }
42
+
43
+ export interface BenefitInput {
44
+ /** Cache-hit discount rate α ∈ (0,1); validated upstream by config parsing. */
45
+ readonly alpha: number
46
+ /** Token mass of the protected tail that must be refilled after a mutation. */
47
+ readonly tailTokens: number
48
+ /** Estimated remaining turns Ŝ; `undefined` keeps expectedSaving out of the result. */
49
+ readonly remainingTurns?: number | undefined
50
+ /** True for fresh-stage batches: their content was never served, so it is
51
+ * not in the KV cache and shaping it causes no cache break — no refill
52
+ * penalty applies and the whole discounted recovery is pure gain. */
53
+ readonly refillPenaltyExempt?: boolean | undefined
54
+ }
55
+
56
+ export interface BenefitEstimate {
57
+ /** Net reclaimed tokens across the batch; may be ≤ 0 when a batch is not worth it. */
58
+ readonly recoveredTokens: number
59
+ /** The one-time cache-refill penalty the merged mutation pays: (1−α)·tailTokens. */
60
+ readonly penaltyTokens: number
61
+ /** Turns of discounted recovery needed to recoup the penalty; `undefined` when α·R ≤ 0. */
62
+ readonly paybackTurns?: number
63
+ /** Discounted net benefit over the remaining session; omitted when Ŝ is unknown. */
64
+ readonly expectedSaving?: number
65
+ }
66
+
67
+ /**
68
+ * The band one priced batch falls into. Advisory vocabulary only — every band
69
+ * lands; the label says what the model thought of the landing, not whether it
70
+ * was allowed to happen.
71
+ */
72
+ export type AdviceBand =
73
+ /** Payback ≤ 1 turn, or ≤ a quarter of the estimated remaining turns. */
74
+ | 'profitable'
75
+ /** At least one candidate reaches the high-impact threshold: the batch is
76
+ * large enough that a human would want to know it moved. */
77
+ | 'high-impact'
78
+ /** Ŝ known and payback ∈ (1, 3]: it does pay back, but slowly. */
79
+ | 'slow-payback'
80
+ /** α·R ≤ 0: the accounting has no discounted recovery to argue from. */
81
+ | 'unpriceable'
82
+ /** Ŝ known and payback > 3 turns: the model would not have spent the cache break. */
83
+ | 'not-worth-it'
84
+
85
+ export interface AdviceInput {
86
+ /** Cache-hit discount rate α ∈ (0,1); validated upstream by config parsing. */
87
+ readonly alpha: number
88
+ /** Token mass of the protected tail that must be refilled after a mutation. */
89
+ readonly tailTokens: number
90
+ /** Candidates at or above this token impact are labelled `high-impact`. */
91
+ readonly highImpactTokens: number
92
+ /** Estimated remaining turns Ŝ; `undefined` closes the slow-payback band. */
93
+ readonly remainingTurns?: number | undefined
94
+ /** Landing stage of the batch: `'fresh'` batches are priced without the
95
+ * tail-refill penalty (first-exposure shaping causes no cache break);
96
+ * `'history'` batches — already-served content — pay it in full. */
97
+ readonly stage?: 'fresh' | 'history' | undefined
98
+ }
99
+
100
+ export interface AdviceResult {
101
+ readonly band: AdviceBand
102
+ /** The full benefit estimate the band was derived from. */
103
+ readonly benefit: BenefitEstimate
104
+ /** Number of candidates that carried a positive recovery and were priced. */
105
+ readonly priced: number
106
+ /** Largest single-candidate token mass in the batch (the high-impact evidence). */
107
+ readonly maxTokensBefore: number
108
+ }
109
+
110
+ /**
111
+ * Shipped defaults of the advisory model. They are constants rather than
112
+ * settings keys because the machine no longer acts on them: after the review
113
+ * gate was retired these numbers only shape a label, so exposing them as
114
+ * tunable configuration would advertise a control that changes no behavior.
115
+ */
116
+ export const DEFAULT_ADVICE_ALPHA = 0.1
117
+ export const DEFAULT_ADVICE_HIGH_IMPACT_TOKENS = 4_000
118
+
119
+ /**
120
+ * Aggregate the batch-level benefit of a set of reduction candidates.
121
+ *
122
+ * Individual candidates whose replacement would grow the context contribute
123
+ * zero recovery (they never make a batch look better than dropping them).
124
+ */
125
+ export function computeBenefit(candidates: readonly BenefitCandidate[], input: BenefitInput): BenefitEstimate {
126
+ const { alpha, tailTokens, remainingTurns } = input
127
+ let recoveredTokens = 0
128
+ for (const candidate of candidates) {
129
+ recoveredTokens += Math.max(0, candidate.tokensBefore - candidate.tokensAfter)
130
+ }
131
+ const penaltyTokens = input.refillPenaltyExempt === true ? 0 : (1 - alpha) * tailTokens
132
+ const perTurnSaving = alpha * recoveredTokens
133
+ if (perTurnSaving <= 0) {
134
+ return remainingTurns === undefined
135
+ ? { recoveredTokens, penaltyTokens }
136
+ : { recoveredTokens, penaltyTokens, expectedSaving: -penaltyTokens }
137
+ }
138
+ const paybackTurns = penaltyTokens / perTurnSaving
139
+ if (remainingTurns === undefined) {
140
+ return { recoveredTokens, penaltyTokens, paybackTurns }
141
+ }
142
+ return {
143
+ recoveredTokens,
144
+ penaltyTokens,
145
+ paybackTurns,
146
+ expectedSaving: perTurnSaving * Math.max(0, remainingTurns - paybackTurns),
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Label one batch of planned replacements.
152
+ *
153
+ * Pipeline: zero/negative-recovery candidates are priced out first (they never
154
+ * make a batch look better), the surviving batch is priced once through
155
+ * `computeBenefit`, and the band is a batch decision — the refill penalty is a
156
+ * property of the landing event, not of any single candidate, so pricing per
157
+ * candidate would overstate payback N-fold.
158
+ *
159
+ * Band precedence (identical thresholds to the retired triage model):
160
+ * - α too small to price a payback → `unpriceable`;
161
+ * - any candidate at `highImpactTokens` → `high-impact`;
162
+ * - `paybackTurns ≤ 1`, or Ŝ known and `paybackTurns ≤ 0.25·Ŝ` → `profitable`;
163
+ * - Ŝ known and `paybackTurns ∈ (1, 3]` → `slow-payback`;
164
+ * - everything else → `not-worth-it`.
165
+ *
166
+ * @param candidates - planned replacements of one pass, in any order.
167
+ * @param input - pricing inputs and the stage of the batch.
168
+ * @returns the advice, or `undefined` when nothing carried a positive recovery.
169
+ */
170
+ export function adviseCandidates(
171
+ candidates: readonly BenefitCandidate[],
172
+ input: AdviceInput,
173
+ ): AdviceResult | undefined {
174
+ const usable: BenefitCandidate[] = []
175
+ for (const candidate of candidates) {
176
+ if (Math.max(0, candidate.tokensBefore - candidate.tokensAfter) <= 0) continue
177
+ usable.push(candidate)
178
+ }
179
+ if (usable.length === 0) return undefined
180
+
181
+ const benefit = computeBenefit(usable, {
182
+ alpha: input.alpha,
183
+ tailTokens: input.tailTokens,
184
+ ...input.remainingTurns !== undefined ? { remainingTurns: input.remainingTurns } : {},
185
+ refillPenaltyExempt: input.stage === 'fresh',
186
+ })
187
+ const payback = benefit.paybackTurns
188
+ const maxTokensBefore = usable.reduce((max, candidate) => Math.max(max, candidate.tokensBefore), 0)
189
+ const band: AdviceBand = payback === undefined
190
+ ? 'unpriceable'
191
+ : maxTokensBefore >= input.highImpactTokens
192
+ ? 'high-impact'
193
+ : payback <= 1
194
+ || (input.remainingTurns !== undefined && payback <= 0.25 * input.remainingTurns)
195
+ ? 'profitable'
196
+ : input.remainingTurns !== undefined && payback <= 3
197
+ ? 'slow-payback'
198
+ : 'not-worth-it'
199
+ return { band, benefit, priced: usable.length, maxTokensBefore }
200
+ }
@@ -85,18 +85,6 @@ export interface PresetOptions {
85
85
  readonly sampleLimit: number
86
86
  readonly minTokens: number
87
87
  }
88
- /**
89
- * Human-gated review pipeline (beta): edge/high-impact candidates queue for
90
- * manual approval and execute in one merged batch at the next turn boundary
91
- * instead of the automatic path (R4).
92
- */
93
- readonly reviewMode: boolean
94
- /** Turn-boundary patience: pending review proposals older than this many turns auto-expire (R4). */
95
- readonly reviewTimeoutTurns: number
96
- /** Cache-hit discount rate α in the benefit model; expectedSaving = α·R·Ŝ − (1−α)·tail. */
97
- readonly cacheHitDiscountAlpha: number
98
- /** Candidates whose tokenBefore reaches this threshold bypass payback triage and always enter review (R4). */
99
- readonly reviewHighImpactTokens: number
100
88
  }
101
89
 
102
90
  /** Common user-authored Custom stages shared by persisted policy versions. */
@@ -154,11 +142,6 @@ export interface PresetOptionsSettings {
154
142
  readonly prefixStabilizer?: boolean
155
143
  readonly readState?: boolean
156
144
  readonly estimatorMode?: '' | 'host' | 'direct'
157
- /** Review-mode overrides (beta); see PresetOptions.reviewMode. */
158
- readonly reviewMode?: boolean
159
- readonly reviewTimeoutTurns?: number
160
- readonly cacheHitDiscountAlpha?: number
161
- readonly reviewHighImpactTokens?: number
162
145
  /**
163
146
  * Estimator endpoint fields. Persisted-settings only: they never enter the
164
147
  * frozen CompressionPolicy, which is emitted verbatim by policy-resolved
@@ -34,11 +34,15 @@ describe('built Harness client artifact', () => {
34
34
  }) as { apply?: unknown, inject?: unknown }
35
35
 
36
36
  expect(exported.apply).toBeTypeOf('function')
37
- // 0.1.5 declares only the always-present `slots` service: `locale` and
38
- // `settingsScope` are resolved lazily inside apply, because a declarative
39
- // inject of an absent or late service suspends apply forever and the whole
40
- // UI disappears without a trace.
41
- expect(exported.inject).toEqual(['slots'])
37
+ // Declare every consumed service: cordis holds apply until each one is
38
+ // provided, which is the official client-plugin pattern on this 0.1.5 host
39
+ // line (dsh-thinking-levels does the same). The earlier lazy alternative
40
+ // resolving `locale` / `settingsScope` through ctx.get() inside apply —
41
+ // raced the settings client's activation; on a loss the apply
42
+ // early-returned and EVERY settings entry (the standalone section, the
43
+ // plugins-tab card, the item card) silently vanished. See the rationale
44
+ // recorded in src/client/index.ts.
45
+ expect(exported.inject).toEqual(['slots', 'locale', 'settingsScope'])
42
46
  const style = document.querySelector<HTMLStyleElement>(
43
47
  'style[data-plugin-css="dsh-context-compression-improved/CompressionProfileSelector.module.css"]',
44
48
  )
@@ -148,35 +148,19 @@ describe('presetOptions writes are path-addressed', () => {
148
148
  expect(writes).toEqual([[{ op: 'set', path: ['presetOptions', 'estimatorProvider'], value: 'local-35b' }]])
149
149
  })
150
150
 
151
- it('writes and clears the review-mode fields without touching estimator siblings', async () => {
151
+ it('treats a retired review-gate patch as a no-op instead of a write', async () => {
152
152
  const { injected, writes, snapshot } = bindInjected({ estimatorMode: 'host' })
153
+ // The patch surface no longer carries these keys, and the decoders accept
154
+ // them only so a legacy document keeps loading. Cast past the removed type
155
+ // so the test pins the RUNTIME behaviour: nothing is written.
153
156
  await injected.savePresetOptions({
154
157
  reviewMode: true,
155
158
  reviewTimeoutTurns: 8,
156
159
  cacheHitDiscountAlpha: 0.2,
157
160
  reviewHighImpactTokens: 6000,
158
- })
159
- expect(snapshot()['presetOptions']).toEqual({
160
- estimatorMode: 'host',
161
- reviewMode: true,
162
- reviewTimeoutTurns: 8,
163
- cacheHitDiscountAlpha: 0.2,
164
- reviewHighImpactTokens: 6000,
165
- })
166
- await injected.savePresetOptions({ reviewMode: undefined })
167
- expect(snapshot()['presetOptions']).toEqual({
168
- estimatorMode: 'host',
169
- reviewTimeoutTurns: 8,
170
- cacheHitDiscountAlpha: 0.2,
171
- reviewHighImpactTokens: 6000,
172
- })
173
- for (const batch of writes) {
174
- for (const op of batch) {
175
- expect(op.path).toEqual(['presetOptions', op.path[1] ?? ''])
176
- expect(['reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens'])
177
- .toContain(op.path[1])
178
- }
179
- }
161
+ } as unknown as Parameters<typeof injected.savePresetOptions>[0])
162
+ expect(writes).toEqual([])
163
+ expect(snapshot()['presetOptions']).toEqual({ estimatorMode: 'host' })
180
164
  })
181
165
 
182
166
  it('never replaces the whole section, so sibling overrides survive', async () => {