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.
Files changed (57) hide show
  1. package/.gitattributes +1 -0
  2. package/CHANGELOG.ja.md +144 -83
  3. package/CHANGELOG.ko.md +143 -82
  4. package/CHANGELOG.md +278 -212
  5. package/CHANGELOG.zh.md +131 -77
  6. package/docs/installation.md +103 -103
  7. package/docs/installation.zh.md +100 -100
  8. package/package.json +1 -1
  9. package/packages/selector/cordis.patch.yml +5 -6
  10. package/packages/selector/lib/advisor-state.js +4 -231
  11. package/packages/selector/lib/client.d.ts +0 -24
  12. package/packages/selector/lib/client.js +6 -501
  13. package/packages/selector/lib/index.d.ts +4 -10
  14. package/packages/selector/lib/index.js +16 -234
  15. package/packages/selector/lib/pruner.d.ts +13 -248
  16. package/packages/selector/lib/pruner.js +148 -552
  17. package/packages/selector/src/client/EstimatorControls.tsx +0 -101
  18. package/packages/selector/src/client/index.ts +0 -17
  19. package/packages/selector/src/client/locales.ts +0 -38
  20. package/packages/selector/src/client/preset-options.ts +3 -2
  21. package/packages/selector/src/client/settings-section.tsx +8 -17
  22. package/packages/selector/src/index.ts +24 -271
  23. package/packages/selector/src/profiles.ts +4 -27
  24. package/packages/selector/src/pruner/state.ts +2 -25
  25. package/packages/selector/src/pruner.ts +75 -403
  26. package/packages/selector/src/runtime/audit.ts +27 -21
  27. package/packages/selector/src/runtime/config.ts +6 -32
  28. package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
  29. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
  30. package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
  31. package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
  32. package/packages/selector/src/runtime/types.ts +0 -17
  33. package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
  34. package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
  35. package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
  36. package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
  37. package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
  38. package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
  39. package/packages/selector/tests/runtime/audit.spec.ts +35 -21
  40. package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
  41. package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
  42. package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
  43. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
  44. package/packages/selector/tests/settings-seat.client.spec.ts +45 -14
  45. package/scripts/toolclass-corpus-replay.mjs +281 -281
  46. package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
  47. package/packages/selector/src/client/review-scope.ts +0 -16
  48. package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
  49. package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
  50. package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
  51. package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
  52. package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
  53. package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
  54. package/packages/selector/tests/review-routes.host.spec.ts +0 -290
  55. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
  56. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
  57. package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
@@ -0,0 +1,232 @@
1
+ /**
2
+ * The retired review gate's replacement, pinned at the host integration level.
3
+ *
4
+ * This spec exists to make the OLD failure impossible to reintroduce: with the
5
+ * exact settings that used to divert 100% of a fresh batch into the human-gated
6
+ * review queue (`reviewMode: true` + a `reviewHighImpactTokens` threshold far
7
+ * below the batch), the batch must LAND, and the benefit model must publish its
8
+ * opinion as a `reduction-advice` audit instead of withholding anything.
9
+ *
10
+ * "缩减不阻断自动处理": advice describes a landing, it never gates one.
11
+ */
12
+ import { describe, expect, it, vi } from 'vitest'
13
+ import { Context } from '@deepseek-ai/cordis'
14
+ import {
15
+ ToolCallId as CallId,
16
+ createMessage,
17
+ createUserMessage,
18
+ createToolResultMessage,
19
+ } from '@deepseek-ai/dsh-llm'
20
+ import SessionStore, {
21
+ Session,
22
+ SessionId,
23
+ canonicalHeader,
24
+ } from '@deepseek-ai/dsh-session'
25
+ import {
26
+ SettingsProvider,
27
+ type SettingsNamespace,
28
+ } from '@deepseek-ai/dsh-settings'
29
+ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
30
+ import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
31
+ import TokenMeter from '@deepseek-ai/dsh-token-meter'
32
+ import ToolRuntime from '@deepseek-ai/dsh-tools'
33
+ import * as SelectorHost from '../../src/index.ts'
34
+ import ToolResultPruner, {
35
+ CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
36
+ } from '../../src/pruner.ts'
37
+ import { measureForCompaction } from '../../src/runtime/measurement.ts'
38
+ import {
39
+ COMPRESSION_AUDIT_PREFIX,
40
+ type CompressionAuditRecord,
41
+ type CompressionRewriteAuditRecord,
42
+ type ReductionAdviceAuditRecord,
43
+ } from '../../src/runtime/audit.ts'
44
+
45
+ const MODEL = 'deepseek-v4-flash'
46
+
47
+ class TestSettings extends SettingsProvider {
48
+ readonly writable = true
49
+ private readonly stored: Record<string, unknown> = {}
50
+
51
+ protected override load(): Promise<Record<string, unknown>> {
52
+ return Promise.resolve(structuredClone(this.stored))
53
+ }
54
+
55
+ protected override persist(namespace: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
56
+ this.stored[namespace] = structuredClone(section)
57
+ return Promise.resolve()
58
+ }
59
+ }
60
+
61
+ async function runtimeContext(): Promise<Context> {
62
+ const ctx = new Context()
63
+ await ctx.plugin(SessionStore).await()
64
+ await ctx.plugin(SystemPrompt).await()
65
+ await ctx.plugin(ToolRuntime).await()
66
+ await ctx.plugin(SessionProjectionRegistry).await()
67
+ await ctx.plugin(TokenMeter).await()
68
+ return ctx
69
+ }
70
+
71
+ function captureAudit(ctx: Context): { records(): CompressionAuditRecord[] } {
72
+ const info = vi.spyOn(ctx.logger, 'info').mockImplementation(() => ctx.logger)
73
+ return {
74
+ records: () => info.mock.calls.flatMap((call) => {
75
+ const line = String(call[0])
76
+ return line.startsWith(COMPRESSION_AUDIT_PREFIX)
77
+ ? [JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord]
78
+ : []
79
+ }),
80
+ }
81
+ }
82
+
83
+ function rewrites(records: readonly CompressionAuditRecord[]): CompressionRewriteAuditRecord[] {
84
+ return records.filter((record): record is CompressionRewriteAuditRecord => record.kind === 'rewrite')
85
+ }
86
+
87
+ function adviceOf(records: readonly CompressionAuditRecord[]): ReductionAdviceAuditRecord[] {
88
+ return records.filter((record): record is ReductionAdviceAuditRecord => record.kind === 'reduction-advice')
89
+ }
90
+
91
+ const nsBrand = (value: string): SettingsNamespace => value as unknown as SettingsNamespace
92
+
93
+ function appendToolTurn(session: Session, turn: number, text: string): void {
94
+ const callId = CallId(`call-${String(turn)}`)
95
+ session.append('turn/start', { turn })
96
+ if (session.requestHeader() === undefined) {
97
+ session.append('request/header', {
98
+ reason: 'initial',
99
+ header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
100
+ })
101
+ }
102
+ session.append('user/message', createUserMessage({
103
+ content: [{ type: 'text', text: `user turn ${String(turn)}` }],
104
+ source: { kind: 'user' },
105
+ }), { surfaceOp: 'append' })
106
+ session.append('step/start', { turn, step: 1 })
107
+ session.append('assistant/message', {
108
+ stream: [],
109
+ turn,
110
+ step: 1,
111
+ message: createMessage({
112
+ role: 'assistant',
113
+ content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
114
+ source: { kind: 'model', provider: 'deepseek', model: MODEL },
115
+ }),
116
+ }, { surfaceOp: 'append' })
117
+ session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' })
118
+ session.append('tool/result', {
119
+ turn,
120
+ step: 1,
121
+ message: createToolResultMessage({
122
+ callId,
123
+ content: [{ type: 'text', text }],
124
+ isError: false,
125
+ }),
126
+ }, { surfaceOp: 'append' })
127
+ session.append('step/end', { turn, step: 1 })
128
+ }
129
+
130
+ /**
131
+ * The settings that used to withhold everything: review mode ON with a
132
+ * high-impact threshold of 1 token. History pricing is kept production-like
133
+ * (64,000 protected tail tokens) so the batch is only landable through the
134
+ * fresh stage.
135
+ */
136
+ async function gatedSettings(ctx: Context): Promise<void> {
137
+ await ctx.plugin(TestSettings).await()
138
+ await ctx.plugin(SelectorHost).await()
139
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
140
+ profile: 'tokenpilot-inspired',
141
+ presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 },
142
+ })
143
+ await ctx.plugin(ToolResultPruner, {
144
+ profile: 'tokenpilot-inspired',
145
+ freshTriggerTokens: 200,
146
+ freshTargetTokens: 100,
147
+ aggregateTriggerTokens: 1_000_000,
148
+ aggregateTargetTokens: 900_000,
149
+ historyTriggerTokens: 400,
150
+ historyKeepRecentToolCalls: 0,
151
+ historyKeepRecentTokens: 64_000,
152
+ historyMinReclaimTokens: 1,
153
+ }).await()
154
+ }
155
+
156
+ function freshSession(ctx: Context, id: string, text: string): Session {
157
+ const session = Session.create(SessionId(id))
158
+ appendToolTurn(session, 1, text)
159
+ const total = measureForCompaction(ctx, session).totalTokens
160
+ session.append('request/context', {
161
+ provider: 'deepseek',
162
+ model: MODEL,
163
+ contextWindow: Math.floor(total / 0.6),
164
+ })
165
+ // Fresh landing publishes a surface replacement, which requires an open turn.
166
+ session.append('turn/start', { turn: 2 })
167
+ return session
168
+ }
169
+
170
+ /** ~36,400 characters. The shipped advice threshold is 4,000 tokens, and this
171
+ * fixture is repetitive enough that the exact tokenizer packs it denser than
172
+ * the conservative 4-chars-per-token estimate (an 18,200-character variant
173
+ * measured under 4,000 tokens), so the volume is doubled to keep the
174
+ * `high-impact` label deterministic. */
175
+ const HIGH_IMPACT_TEXT = 'fresh reviewable evidence '.repeat(1_400)
176
+
177
+ describe('advice never withholds (retired review gate)', () => {
178
+ it('lands the fresh batch the gate used to divert, and advises instead', async () => {
179
+ const ctx = await runtimeContext()
180
+ await gatedSettings(ctx)
181
+ const audit = captureAudit(ctx)
182
+ const session = freshSession(ctx, 'advice-lands', HIGH_IMPACT_TEXT)
183
+
184
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
185
+ const records = audit.records()
186
+
187
+ // 1. The reduction LANDED. Under the retired gate this was the assertion
188
+ // that failed: the batch was withheld pending human approval.
189
+ expect(result.pruned).toHaveLength(1)
190
+ expect(rewrites(records).some(entry => entry.component === 'fresh')).toBe(true)
191
+
192
+ // 2. The benefit model still speaks — as advice about the landing.
193
+ const advice = adviceOf(records)
194
+ expect(advice).toHaveLength(1)
195
+ expect(advice[0]!.stage).toBe('fresh')
196
+ expect(advice[0]!.band).toBe('high-impact')
197
+ expect(advice[0]!.maxTokensBefore).toBeGreaterThanOrEqual(4_000)
198
+ expect(advice[0]!.recoveredTokens).toBeGreaterThan(0)
199
+ // The batch it describes is exactly the batch that landed.
200
+ const landedSeqs = rewrites(records).flatMap(entry => [...entry.sourceSeqs])
201
+ expect(advice[0]!.itemSeqs.every(seq => landedSeqs.includes(seq))).toBe(true)
202
+
203
+ // 3. Every advice record describes a landing, never a pending decision.
204
+ expect(advice.length).toBeLessThanOrEqual(rewrites(records).length)
205
+ })
206
+
207
+ it('keeps the automatic path identical when no legacy gate key is present', async () => {
208
+ const ctx = await runtimeContext()
209
+ await ctx.plugin(TestSettings).await()
210
+ await ctx.plugin(SelectorHost).await()
211
+ await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
212
+ profile: 'tokenpilot-inspired',
213
+ })
214
+ await ctx.plugin(ToolResultPruner, {
215
+ profile: 'tokenpilot-inspired',
216
+ freshTriggerTokens: 200,
217
+ freshTargetTokens: 100,
218
+ aggregateTriggerTokens: 1_000_000,
219
+ aggregateTargetTokens: 900_000,
220
+ historyTriggerTokens: 400,
221
+ historyKeepRecentToolCalls: 0,
222
+ historyKeepRecentTokens: 64_000,
223
+ historyMinReclaimTokens: 1,
224
+ }).await()
225
+ const audit = captureAudit(ctx)
226
+ const session = freshSession(ctx, 'advice-default', HIGH_IMPACT_TEXT)
227
+
228
+ const result = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
229
+ expect(result.pruned).toHaveLength(1)
230
+ expect(adviceOf(audit.records())).toHaveLength(1)
231
+ })
232
+ })