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,393 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- classifyCandidates,
4
- computeBenefit,
5
- proposalId,
6
- } from '../../../src/runtime/tokenpilot/proposal.ts'
7
-
8
- /** Reference values follow the spec formulas exactly:
9
- * R = Σ(before − after); penalty = (1−α)·tail; payback = penalty / (α·R);
10
- * expectedSaving = α·R·max(0, Ŝ − payback). */
11
-
12
- describe('computeBenefit', () => {
13
- it('returns zero recovery and no payback for an empty batch', () => {
14
- const result = computeBenefit([], { alpha: 0.1, tailTokens: 4000 })
15
- expect(result.recoveredTokens).toBe(0)
16
- expect(result.penaltyTokens).toBe(3600)
17
- expect(result.paybackTurns).toBeUndefined()
18
- expect(result.expectedSaving).toBeUndefined()
19
- })
20
-
21
- it('computes the single-candidate benefit with a known remaining-turn estimate', () => {
22
- // R = 4000 − 400 = 3600; penalty = 0.9·4000 = 3600; per-turn = 0.1·3600 = 360
23
- const result = computeBenefit(
24
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
25
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 12 },
26
- )
27
- expect(result.recoveredTokens).toBe(3600)
28
- expect(result.penaltyTokens).toBeCloseTo(3600)
29
- expect(result.paybackTurns).toBeCloseTo(10)
30
- // α·R·max(0, 12 − 10) = 360·2
31
- expect(result.expectedSaving).toBeCloseTo(720)
32
- })
33
-
34
- it('omits expectedSaving while Ŝ is unknown', () => {
35
- const result = computeBenefit(
36
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
37
- { alpha: 0.1, tailTokens: 4000 },
38
- )
39
- expect(result.paybackTurns).toBeCloseTo(10)
40
- expect(result.expectedSaving).toBeUndefined()
41
- })
42
-
43
- it('merges candidates into one batch so the penalty is paid once', () => {
44
- const result = computeBenefit(
45
- [
46
- { sourceSeq: 1, tokensBefore: 2000, tokensAfter: 500 },
47
- { sourceSeq: 2, tokensBefore: 3000, tokensAfter: 1000 },
48
- ],
49
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 20 },
50
- )
51
- expect(result.recoveredTokens).toBe(3500)
52
- expect(result.penaltyTokens).toBeCloseTo(3600)
53
- expect(result.paybackTurns).toBeCloseTo(3600 / 350)
54
- expect(result.expectedSaving).toBeCloseTo(350 * (20 - 3600 / 350))
55
- })
56
-
57
- it('clamps a growing candidate to zero recovery instead of negative batch credit', () => {
58
- const result = computeBenefit(
59
- [{ sourceSeq: 3, tokensBefore: 100, tokensAfter: 400 }],
60
- { alpha: 0.1, tailTokens: 4000 },
61
- )
62
- expect(result.recoveredTokens).toBe(0)
63
- expect(result.paybackTurns).toBeUndefined()
64
- })
65
-
66
- it('stays finite as α → 0: no division, payback undefined, expectedSaving negative', () => {
67
- const result = computeBenefit(
68
- [{ sourceSeq: 4, tokensBefore: 4000, tokensAfter: 400 }],
69
- { alpha: 0, tailTokens: 4000, remainingTurns: 12 },
70
- )
71
- expect(result.recoveredTokens).toBe(3600)
72
- expect(result.paybackTurns).toBeUndefined()
73
- // With zero per-turn saving the batch can only lose the refill penalty.
74
- expect(result.expectedSaving).toBeCloseTo(-4000)
75
- })
76
-
77
- it('never reports a positive expectedSaving once Ŝ is inside the payback window', () => {
78
- // payback = 10 turns; Ŝ = 5 → max(0, Ŝ − payback) = 0
79
- const result = computeBenefit(
80
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
81
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 5 },
82
- )
83
- expect(result.expectedSaving).toBeCloseTo(0)
84
- })
85
-
86
- it('waives the refill penalty for a refill-exempt batch: payback 0, saving α·R·Ŝ', () => {
87
- // Fresh-stage shaping: the content never entered the KV cache, so no
88
- // refill penalty applies — the whole discounted recovery is pure gain.
89
- const result = computeBenefit(
90
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
91
- { alpha: 0.1, tailTokens: 4000, remainingTurns: 12, refillPenaltyExempt: true },
92
- )
93
- expect(result.recoveredTokens).toBe(3600)
94
- expect(result.penaltyTokens).toBe(0)
95
- expect(result.paybackTurns).toBe(0)
96
- // α·R·max(0, Ŝ − 0) = 360·12
97
- expect(result.expectedSaving).toBeCloseTo(4320)
98
- })
99
-
100
- it('keeps the refill penalty when the exemption flag is absent', () => {
101
- const result = computeBenefit(
102
- [{ sourceSeq: 7, tokensBefore: 4000, tokensAfter: 400 }],
103
- { alpha: 0.1, tailTokens: 4000 },
104
- )
105
- expect(result.penaltyTokens).toBeCloseTo(3600)
106
- })
107
- })
108
-
109
- describe('proposalId', () => {
110
- it('is stable for the same digest list and length-sensitive', () => {
111
- const digests = ['aa'.repeat(32), 'bb'.repeat(32)]
112
- expect(proposalId(digests)).toBe(proposalId([...digests]))
113
- expect(proposalId(digests)).toHaveLength(12)
114
- expect(proposalId(digests)).not.toBe(proposalId([digests[0]!, digests[1]!, digests[0]!]))
115
- })
116
-
117
- it('distinguishes an empty batch from a single digest', () => {
118
- expect(proposalId([])).not.toBe(proposalId(['cc'.repeat(32)]))
119
- })
120
- })
121
-
122
- /** Triage fixtures: α=0.5 keeps the arithmetic obvious —
123
- * payback = 0.5·tail / (0.5·R) = tail / R. */
124
- const TRIAGE = { alpha: 0.5, tailTokens: 1000, reviewHighImpactTokens: 4000 }
125
-
126
- function candidate(overrides: {
127
- sourceSeq?: number
128
- tokensBefore?: number
129
- tokensAfter?: number
130
- reducer?: string
131
- component?: string
132
- }): Parameters<typeof classifyCandidates>[0][number] {
133
- return {
134
- sourceSeq: overrides.sourceSeq ?? 1,
135
- tokensBefore: overrides.tokensBefore ?? 1000,
136
- tokensAfter: overrides.tokensAfter ?? 500,
137
- reducer: overrides.reducer ?? 'native-whole-result',
138
- component: overrides.component ?? 'native-tool-result',
139
- content: [{ type: 'text', text: 'opaque result body' }],
140
- }
141
- }
142
-
143
- describe('classifyCandidates', () => {
144
- it('auto-applies the clearly profitable band and drops negative recovery', () => {
145
- // tail/R = 1000/1000 = 1 → payback = 1 → auto; R = 0 → drop.
146
- const result = classifyCandidates(
147
- [candidate({ sourceSeq: 1, tokensBefore: 2000, tokensAfter: 1000 }), candidate({ sourceSeq: 2, tokensBefore: 500, tokensAfter: 500 })],
148
- TRIAGE,
149
- )
150
- expect(result.auto.map(entry => entry.sourceSeq)).toEqual([1])
151
- expect(result.drop.map(entry => entry.sourceSeq)).toEqual([2])
152
- expect(result.review).toEqual([])
153
- })
154
-
155
- it('sends high-impact candidates to review even inside the auto band', () => {
156
- // payback = 1000/5000 = 0.2 ≤ 1, but tokensBefore ≥ 4000 → review ("直接送审").
157
- const result = classifyCandidates(
158
- [candidate({ tokensBefore: 5000, tokensAfter: 4000 })],
159
- TRIAGE,
160
- )
161
- expect(result.auto).toEqual([])
162
- expect(result.review).toHaveLength(1)
163
- expect(result.review[0]!.items[0]!.tokensBefore).toBe(5000)
164
- })
165
-
166
- it('keeps the Ŝ-gated edge band in review and closes it while Ŝ is unknown', () => {
167
- // payback = 1000/200 = 5: with Ŝ = 40 → 5 ≤ 0.25·40 = 10 → auto.
168
- const known = classifyCandidates(
169
- [candidate({ tokensBefore: 1200, tokensAfter: 1000 })],
170
- { ...TRIAGE, remainingTurns: 40 },
171
- )
172
- expect(known.auto).toHaveLength(1)
173
- // payback = 1000/50 = 20: with Ŝ = 40, 20 > 0.25·40 and > 3 → drop.
174
- const slow = classifyCandidates(
175
- [candidate({ tokensBefore: 1050, tokensAfter: 1000 })],
176
- { ...TRIAGE, remainingTurns: 40 },
177
- )
178
- expect(slow.drop).toHaveLength(1)
179
- // Same slow candidate with Ŝ unknown → no edge band, no auto → drop.
180
- const unknown = classifyCandidates(
181
- [candidate({ tokensBefore: 1050, tokensAfter: 1000 })],
182
- TRIAGE,
183
- )
184
- expect(unknown.drop).toHaveLength(1)
185
- // payback = 1000/400 = 2.5: Ŝ = 8 → 2.5 > 0.25·8 = 2 and ≤ 3 → review.
186
- const edge = classifyCandidates(
187
- [candidate({ tokensBefore: 1400, tokensAfter: 1000 })],
188
- { ...TRIAGE, remainingTurns: 8 },
189
- )
190
- expect(edge.review).toHaveLength(1)
191
- // Same edge candidate with Ŝ unknown → drop.
192
- const edgeUnknown = classifyCandidates(
193
- [candidate({ tokensBefore: 1400, tokensAfter: 1000 })],
194
- TRIAGE,
195
- )
196
- expect(edgeUnknown.drop).toHaveLength(1)
197
- })
198
-
199
- it('freezes digest, kind, and benefit into the review skeleton', () => {
200
- const result = classifyCandidates(
201
- [candidate({ sourceSeq: 9, reducer: 'dedupe-pointer', component: 'fresh' })],
202
- // payback = 1000/500 = 2: Ŝ = 6 → 2 > 0.25·6 = 1.5 and ≤ 3 → review.
203
- { ...TRIAGE, remainingTurns: 6 },
204
- )
205
- const skeleton = result.review[0]!
206
- expect(skeleton.kind).toBe('dedup')
207
- expect(skeleton.items[0]!.seq).toBe(9)
208
- expect(skeleton.items[0]!.digest).toMatch(/^[0-9a-f]{64}$/)
209
- expect(skeleton.items[0]!.kind).toBe('dedup')
210
- expect(skeleton.benefit.recoveredTokens).toBe(500)
211
- expect(skeleton.id).toEqual(proposalId([skeleton.items[0]!.digest]))
212
- })
213
-
214
- it('labels estimator-channel seqs without changing the triage math', () => {
215
- const result = classifyCandidates(
216
- [candidate({ sourceSeq: 11, reducer: 'superseded-read-whole-result', component: 'history' })],
217
- { ...TRIAGE, remainingTurns: 6, estimatorSeqs: new Set([11]) },
218
- )
219
- expect(result.review[0]!.kind).toBe('estimator')
220
- })
221
-
222
- it('degenerates to the per-candidate behavior for a single-candidate batch', () => {
223
- // payback = 1000/400 = 2.5: Ŝ = 8 → 2.5 > 2 and ≤ 3 → review, one item.
224
- const result = classifyCandidates(
225
- [candidate({ tokensBefore: 1400, tokensAfter: 1000 })],
226
- { ...TRIAGE, remainingTurns: 8 },
227
- )
228
- expect(result.auto).toEqual([])
229
- expect(result.review).toHaveLength(1)
230
- expect(result.review[0]!.items).toHaveLength(1)
231
- })
232
- })
233
-
234
- /** Fresh-stage batches are exempt from the tail-refill penalty: their content
235
- * never entered the KV cache, so shaping it before the first request causes
236
- * no cache break. The same batch that history pricing would drop must land
237
- * auto. Fixture: α=0.1, tail=64,000 → history penalty = 57,600; batch
238
- * R = 2×450 = 900 → history payback = 57,600/90 = 640 > 3, Ŝ unknown →
239
- * drop. Fresh pricing: penalty = 0 → payback = 0 → auto. */
240
- describe('classifyCandidates (fresh-stage refill exemption)', () => {
241
- const FRESH = {
242
- alpha: 0.1,
243
- tailTokens: 64_000,
244
- reviewHighImpactTokens: 4_000,
245
- }
246
-
247
- it('lands a fresh batch auto that history pricing would drop', () => {
248
- const batch = [
249
- candidate({ sourceSeq: 1, tokensBefore: 2000, tokensAfter: 1550 }),
250
- candidate({ sourceSeq: 2, tokensBefore: 2000, tokensAfter: 1550 }),
251
- ]
252
- const history = classifyCandidates(batch, FRESH)
253
- expect(history.drop.map(entry => entry.sourceSeq)).toEqual([1, 2])
254
- const fresh = classifyCandidates(batch, { ...FRESH, stage: 'fresh' })
255
- expect(fresh.auto.map(entry => entry.sourceSeq)).toEqual([1, 2])
256
- expect(fresh.review).toEqual([])
257
- expect(fresh.drop).toEqual([])
258
- })
259
-
260
- it('still routes a high-impact fresh candidate to review', () => {
261
- const result = classifyCandidates(
262
- [candidate({ tokensBefore: 5000, tokensAfter: 4000 })],
263
- { ...FRESH, stage: 'fresh' },
264
- )
265
- expect(result.auto).toEqual([])
266
- expect(result.review).toHaveLength(1)
267
- })
268
-
269
- it('defaults to history pricing when stage is omitted', () => {
270
- const result = classifyCandidates(
271
- [candidate({ sourceSeq: 1, tokensBefore: 2000, tokensAfter: 1550 })],
272
- FRESH,
273
- )
274
- expect(result.drop.map(entry => entry.sourceSeq)).toEqual([1])
275
- })
276
- })
277
-
278
- /** R1 batch-level benefit: one merged mutation pays the tail refill ONCE.
279
- * Fixture: α=0.02, tail=64,000, Ŝ=60. Per candidate R = 50,000 − 500 = 49,500.
280
- * Batch: R = 247,500 → payback = 62,720 / 4,950 ≈ 12.67 ≤ 0.25·60 = 15 → auto.
281
- * Per candidate: payback = 62,720 / 990 ≈ 63.35 > 3 → every one drops. */
282
- describe('classifyCandidates (batch-level benefit, R1)', () => {
283
- const BATCH = {
284
- alpha: 0.02,
285
- tailTokens: 64_000,
286
- reviewHighImpactTokens: 200_000,
287
- remainingTurns: 60,
288
- }
289
-
290
- function batchCandidate(seq: number, overrides: { tokensBefore?: number, tokensAfter?: number, reducer?: string } = {}) {
291
- return candidate({
292
- sourceSeq: seq,
293
- tokensBefore: overrides.tokensBefore ?? 50_000,
294
- tokensAfter: overrides.tokensAfter ?? 500,
295
- ...overrides.reducer !== undefined ? { reducer: overrides.reducer } : {},
296
- })
297
- }
298
-
299
- it('pays the refill penalty once per batch: 5×50k lands auto', () => {
300
- const result = classifyCandidates(
301
- [1, 2, 3, 4, 5].map(seq => batchCandidate(seq)),
302
- BATCH,
303
- )
304
- expect(result.auto.map(entry => entry.sourceSeq)).toEqual([1, 2, 3, 4, 5])
305
- expect(result.review).toEqual([])
306
- expect(result.drop).toEqual([])
307
- })
308
-
309
- it('counter-proof: priced per candidate the same five would all drop', () => {
310
- // Under per-candidate pricing each candidate pays the full refill alone:
311
- // payback = 62,720 / (0.02·49,500) ≈ 63.35 > 3 → drop. The batch-level
312
- // classifier above lands the identical five in auto.
313
- const perCandidate = computeBenefit(
314
- [{ sourceSeq: 1, tokensBefore: 50_000, tokensAfter: 500 }],
315
- BATCH,
316
- )
317
- expect(perCandidate.paybackTurns).toBeGreaterThan(3)
318
- })
319
-
320
- it('a zero-recovery candidate is priced out before the batch verdict', () => {
321
- // Batch of five real candidates + one growing one: the five land auto
322
- // (payback ≈ 12.67) and the zero-recovery one never joins the pricing.
323
- const result = classifyCandidates(
324
- [
325
- ...[1, 2, 3, 4, 5].map(seq => batchCandidate(seq)),
326
- batchCandidate(6, { tokensBefore: 500, tokensAfter: 500 }),
327
- ],
328
- BATCH,
329
- )
330
- expect(result.auto.map(entry => entry.sourceSeq)).toEqual([1, 2, 3, 4, 5])
331
- expect(result.drop.map(entry => entry.sourceSeq)).toEqual([6])
332
- })
333
-
334
- it('high impact covers the whole batch: one ≥H candidate sends everything to review', () => {
335
- const result = classifyCandidates(
336
- [batchCandidate(1), batchCandidate(2, { tokensBefore: 250_000 })],
337
- BATCH,
338
- )
339
- expect(result.auto).toEqual([])
340
- expect(result.drop).toEqual([])
341
- expect(result.review).toHaveLength(1)
342
- expect(result.review[0]!.items.map(item => item.seq)).toEqual([1, 2])
343
- })
344
-
345
- /** Edge-band review fixture: batch R = 3×430,000 = 1,290,000 →
346
- * perTurn = 0.02·R = 25,800 → payback = 62,720/25,800 ≈ 2.43.
347
- * Ŝ = 8 → 0.25·Ŝ = 2: payback ∈ (2, 3] → review. No candidate is
348
- * high-impact (600,000 < reviewHighImpactTokens 1,000,000). */
349
- const EDGE_REVIEW = {
350
- alpha: 0.02,
351
- tailTokens: 64_000,
352
- reviewHighImpactTokens: 1_000_000,
353
- remainingTurns: 8,
354
- }
355
-
356
- function edgeCandidate(seq: number, overrides: { tokensBefore?: number, tokensAfter?: number, reducer?: string } = {}) {
357
- return candidate({
358
- sourceSeq: seq,
359
- tokensBefore: overrides.tokensBefore ?? 600_000,
360
- tokensAfter: overrides.tokensAfter ?? 170_000,
361
- ...overrides.reducer !== undefined ? { reducer: overrides.reducer } : {},
362
- })
363
- }
364
-
365
- it('groups one proposal per kind and the id covers every item digest', () => {
366
- const result = classifyCandidates(
367
- [1, 2, 3].map(seq => edgeCandidate(seq, { reducer: 'dedupe-pointer' })),
368
- EDGE_REVIEW,
369
- )
370
- expect(result.review).toHaveLength(1)
371
- const skeleton = result.review[0]!
372
- expect(skeleton.kind).toBe('dedup')
373
- expect(skeleton.items).toHaveLength(3)
374
- expect(skeleton.id).toBe(proposalId(skeleton.items.map(item => item.digest)))
375
- })
376
-
377
- it('splits review proposals by kind when the batch mixes reducers', () => {
378
- const result = classifyCandidates(
379
- [
380
- edgeCandidate(1, { reducer: 'dedupe-pointer' }),
381
- edgeCandidate(2, { reducer: 'dedupe-pointer' }),
382
- edgeCandidate(3, { reducer: 'native-whole-result' }),
383
- ],
384
- EDGE_REVIEW,
385
- )
386
- expect(result.review).toHaveLength(2)
387
- const digests = result.review.flatMap(skeleton => skeleton.items.map(item => item.digest))
388
- expect(digests).toHaveLength(3)
389
- for (const skeleton of result.review) {
390
- expect(skeleton.id).toBe(proposalId(skeleton.items.map(item => item.digest)))
391
- }
392
- })
393
- })