dsh-context-compression-improved 0.4.0-beta.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.ja.md +68 -36
  2. package/CHANGELOG.ko.md +67 -35
  3. package/CHANGELOG.md +195 -134
  4. package/CHANGELOG.zh.md +64 -36
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +1 -1
  8. package/README.zh.md +1 -1
  9. package/docs/installation.ja.md +2 -2
  10. package/docs/installation.ko.md +2 -2
  11. package/docs/installation.md +103 -78
  12. package/docs/installation.zh.md +100 -77
  13. package/docs/repair-log.md +54 -0
  14. package/package.json +1 -1
  15. package/packages/selector/lib/{config.js → advisor-state.js} +329 -5
  16. package/packages/selector/lib/client.d.ts +7 -0
  17. package/packages/selector/lib/client.js +33 -3
  18. package/packages/selector/lib/index.d.ts +7 -0
  19. package/packages/selector/lib/index.js +112 -3
  20. package/packages/selector/lib/pruner.d.ts +128 -1
  21. package/packages/selector/lib/pruner.js +2802 -1374
  22. package/packages/selector/src/client/ReviewOverlay.tsx +1 -1
  23. package/packages/selector/src/client/index.ts +1 -1
  24. package/packages/selector/src/client/preset-options.ts +2 -0
  25. package/packages/selector/src/index.ts +129 -49
  26. package/packages/selector/src/profiles.ts +48 -0
  27. package/packages/selector/src/pruner/content.ts +18 -5
  28. package/packages/selector/src/pruner/state.ts +3 -0
  29. package/packages/selector/src/pruner/types.ts +23 -5
  30. package/packages/selector/src/pruner.ts +297 -162
  31. package/packages/selector/src/runtime/adaptive-cost.ts +23 -12
  32. package/packages/selector/src/runtime/audit.ts +40 -2
  33. package/packages/selector/src/runtime/config.ts +88 -1
  34. package/packages/selector/src/runtime/measurement.ts +31 -2
  35. package/packages/selector/src/runtime/reducers.ts +1115 -97
  36. package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
  37. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
  38. package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
  39. package/packages/selector/src/runtime/tokenpilot/dedup.ts +1 -1
  40. package/packages/selector/src/runtime/tokenpilot/estimator.ts +8 -118
  41. package/packages/selector/src/runtime/tokenpilot/locator.ts +1 -1
  42. package/packages/selector/src/runtime/tokenpilot/proposal.ts +76 -32
  43. package/packages/selector/src/runtime/tokenpilot/read-state.ts +23 -2
  44. package/packages/selector/src/runtime/tokenpilot/review-registry.ts +117 -0
  45. package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +303 -0
  46. package/packages/selector/src/runtime/toolclass.ts +103 -0
  47. package/packages/selector/src/runtime/types.ts +37 -0
  48. package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
  49. package/packages/selector/tests/public/package-contract.client.spec.ts +2 -1
  50. package/packages/selector/tests/review-routes-registry.host.spec.ts +142 -0
  51. package/packages/selector/tests/runtime/adaptive-cost.spec.ts +7 -7
  52. package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
  53. package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
  54. package/packages/selector/tests/runtime/audit.spec.ts +88 -1
  55. package/packages/selector/tests/runtime/char-basis.spec.ts +30 -0
  56. package/packages/selector/tests/runtime/code-skeleton.spec.ts +14 -3
  57. package/packages/selector/tests/runtime/frequency-longstrings.spec.ts +74 -0
  58. package/packages/selector/tests/runtime/html-reducer.spec.ts +212 -0
  59. package/packages/selector/tests/runtime/line-mapping.spec.ts +153 -0
  60. package/packages/selector/tests/runtime/prose-reducers.spec.ts +133 -0
  61. package/packages/selector/tests/runtime/public/public-runtime.spec.ts +198 -27
  62. package/packages/selector/tests/runtime/read-input-cap.spec.ts +33 -0
  63. package/packages/selector/tests/runtime/search-reducer.spec.ts +110 -0
  64. package/packages/selector/tests/runtime/sidechannel.spec.ts +241 -0
  65. package/packages/selector/tests/runtime/toc-and-bundled.spec.ts +159 -0
  66. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
  67. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +194 -0
  68. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +70 -1
  69. package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +24 -0
  70. package/packages/selector/tests/runtime/toolclass.spec.ts +156 -0
  71. package/scripts/toolclass-corpus-replay.mjs +281 -0
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Tool-source classification (R1/R2), narrowed to FOUR classes (tasks v2 G4):
3
+ * `read` / `shell` / `search` / `path-listing`, everything else `generic`.
4
+ *
5
+ * Form routing beats identity routing — most tools' results land through
6
+ * `read`/`cat` — so this layer exists to keep the OBSERVED misroutes out of
7
+ * the wrong reducers, not to add per-identity behavior:
8
+ * - `web_search` / `memory_search` are not content-search (findings §0)
9
+ * - `glob` is path listing, not search
10
+ * - `execute_sql` is not shell; `research` is not search (substring era bugs)
11
+ * - MCP output has no predictable identity (`mcp` token → generic, C8)
12
+ *
13
+ * Judgment order (C15): name whitelist → command hit → content fallback, and
14
+ * a hit at any earlier stage is never overridden by a later one.
15
+ */
16
+
17
+ export type ToolClass = 'read' | 'shell' | 'search' | 'path-listing' | 'generic'
18
+
19
+ /** task_11 (AD3): exact-token matching, no substring hits. */
20
+ const TOKEN_SEPARATOR = /[-_/]+/
21
+
22
+ /** Multi-token names that must match as a whole word, not as a token. */
23
+ const READ_NAMES = new Set(['open_file'])
24
+ const READ_TOKENS = new Set(['read', 'cat', 'view'])
25
+ const SHELL_TOKENS = new Set(['shell', 'bash', 'pwsh', 'powershell', 'terminal', 'exec', 'command'])
26
+ const SEARCH_TOKENS = new Set(['grep', 'rg', 'ripgrep'])
27
+ const PATH_LISTING_TOKENS = new Set(['glob', 'tree', 'ls', 'fd', 'find'])
28
+
29
+ const SEARCH_COMMAND_PATTERN = /(?:^|\s)(?:rg|grep|ripgrep)\s/
30
+ const PATH_COMMAND_PATTERN = /(?:^|\s)(?:find|fd|ls|tree)\s/
31
+
32
+ /** grep-style hit line: `path:line[:column][: content]`. */
33
+ const PATH_LINE_CONTENT_PATTERN = /^(.*?):(\d+)(?::\d+)?(?::|\s+-\s+)(.*)$/
34
+
35
+ /**
36
+ * Classify a tool result's source.
37
+ * @param name - raw tool name (case-insensitive).
38
+ * @param command - extracted command argument, '' when absent.
39
+ * @param text - result text; enables the C13/C14 content fallback for names
40
+ * that no whitelist (and no command hit) claims.
41
+ * @returns the tool source class.
42
+ */
43
+ export function classifyToolSource(name: string, command: string, text?: string): ToolClass {
44
+ const lowered = name.toLowerCase()
45
+ const tokens = lowered.split(TOKEN_SEPARATOR).filter(token => token !== '')
46
+ // C8: an `mcp` token means the output identity is arbitrary — generic first,
47
+ // before any content heuristic gets a vote.
48
+ if (tokens.includes('mcp')) return 'generic'
49
+ const byName = classifyByName(lowered, tokens)
50
+ if (byName !== 'generic') return byName
51
+ const byCommand = classifyByCommand(command)
52
+ if (byCommand !== 'generic') return byCommand
53
+ return text === undefined ? 'generic' : classifyByContent(text)
54
+ }
55
+
56
+ function classifyByName(lowered: string, tokens: readonly string[]): ToolClass {
57
+ if (READ_NAMES.has(lowered) || tokens.some(token => READ_TOKENS.has(token))) return 'read'
58
+ if (tokens.some(token => SHELL_TOKENS.has(token))) return 'shell'
59
+ if (tokens.some(token => SEARCH_TOKENS.has(token))) return 'search'
60
+ if (tokens.some(token => PATH_LISTING_TOKENS.has(token))) return 'path-listing'
61
+ return 'generic'
62
+ }
63
+
64
+ function classifyByCommand(command: string): ToolClass {
65
+ if (command === '') return 'generic'
66
+ if (SEARCH_COMMAND_PATTERN.test(command)) return 'search'
67
+ if (PATH_COMMAND_PATTERN.test(command)) return 'path-listing'
68
+ return 'generic'
69
+ }
70
+
71
+ /** A path-shaped hit locator needs a path-looking prefix, not just `:digits`. */
72
+ function looksLikeFilePath(prefix: string): boolean {
73
+ if (prefix.includes('://')) return false
74
+ return prefix.includes('/') || prefix.includes('\\') || /\.[A-Za-z0-9]{1,8}$/.test(prefix)
75
+ }
76
+
77
+ /** A bare path: no whitespace, no URL scheme, with a separator or an extension. */
78
+ function isPurePathLine(line: string): boolean {
79
+ if (/\s/.test(line) || line.includes('://')) return false
80
+ return line.includes('/') || line.includes('\\') || /\.[A-Za-z0-9]{1,8}$/.test(line)
81
+ }
82
+
83
+ /**
84
+ * Content fallback (C13/C14) for unknown names: one path-plausible
85
+ * `path:line:content` line reads as grep output; a body of ≥80% pure paths
86
+ * reads as a directory listing. Both require file-path evidence so prose
87
+ * (`Note: 2024 - ...`) and URLs (`https://host:8080 - ...`) stay generic.
88
+ */
89
+ function classifyByContent(text: string): ToolClass {
90
+ let nonEmpty = 0
91
+ let purePaths = 0
92
+ let pathLineHits = 0
93
+ for (const line of text.split('\n')) {
94
+ if (line.trim() === '') continue
95
+ nonEmpty += 1
96
+ const match = PATH_LINE_CONTENT_PATTERN.exec(line)
97
+ if (match !== null && looksLikeFilePath(match[1] ?? '')) pathLineHits += 1
98
+ if (isPurePathLine(line)) purePaths += 1
99
+ }
100
+ if (pathLineHits >= 1) return 'search'
101
+ if (nonEmpty >= 4 && purePaths / nonEmpty >= 0.8) return 'path-listing'
102
+ return 'generic'
103
+ }
@@ -71,6 +71,20 @@ export interface PresetOptions {
71
71
  readonly readState: boolean
72
72
  /** Optional estimator channel; `''` keeps every estimator consumer on rule-only fallbacks (E1/E2). */
73
73
  readonly estimator: { readonly mode: '' | 'host' | 'direct' }
74
+ /**
75
+ * Advisory relevance advisor: statistics and suggestions only — every output
76
+ * (summaries, scores, decay, recertification) is observational and must never
77
+ * suppress, delay, or rewrite any reduction that would land. `''` (the
78
+ * default) keeps the advisor fully off.
79
+ */
80
+ readonly advisor: {
81
+ readonly mode: '' | 'host' | 'direct'
82
+ readonly timeoutMs: number
83
+ readonly refreshTurns: number
84
+ readonly scoreThreshold: number
85
+ readonly sampleLimit: number
86
+ readonly minTokens: number
87
+ }
74
88
  /**
75
89
  * Human-gated review pipeline (beta): edge/high-impact candidates queue for
76
90
  * manual approval and execute in one merged batch at the next turn boundary
@@ -155,6 +169,13 @@ export interface PresetOptionsSettings {
155
169
  readonly estimatorBaseUrl?: string
156
170
  readonly estimatorApiKey?: string
157
171
  readonly estimatorTimeoutMs?: number
172
+ /** Advisory advisor channel; `''` (the default) keeps the advisor off. */
173
+ readonly advisorMode?: '' | 'host' | 'direct'
174
+ readonly advisorTimeoutMs?: number
175
+ readonly advisorRefreshTurns?: number
176
+ readonly advisorScoreThreshold?: number
177
+ readonly advisorSampleLimit?: number
178
+ readonly advisorMinTokens?: number
158
179
  }
159
180
 
160
181
  /** Durable global preference exposed through `ctx.settings`. */
@@ -199,6 +220,14 @@ export interface ToolResultPruneConfig {
199
220
  historyKeepRecentTokens?: number
200
221
  /** Minimum reclaim required before historical aging is worth a cache break. Profile default when omitted. */
201
222
  historyMinReclaimTokens?: number
223
+ /**
224
+ * Read-class input cap in characters. Invariant (startup-asserted in
225
+ * `resolvePolicy`): when set it must exceed `freshTriggerTokens × 4.0` —
226
+ * the conservative chars/token upper bound — otherwise the cap sits below
227
+ * the fresh trigger and silently silences the fresh path for every read
228
+ * result. Unset profiles (host cap 50k–59.5k chars observed) stay untouched.
229
+ */
230
+ readInputCapChars?: number
202
231
  /**
203
232
  * Auto Compact threshold percent frozen into this deployment by the preset
204
233
  * overlay generation (50–90 integer). When present it supersedes the live
@@ -228,6 +257,12 @@ export interface CompressionPolicy {
228
257
  readonly historyKeepRecentToolCalls: number
229
258
  readonly historyKeepRecentTokens: number
230
259
  readonly historyMinReclaimTokens: number
260
+ /**
261
+ * Read-class input cap in characters when configured; absent otherwise.
262
+ * Startup-asserted to exceed `freshTriggerTokens × 4.0` so it can never
263
+ * silently silence the fresh path (G2 invariant).
264
+ */
265
+ readonly readInputCapChars?: number
231
266
  /**
232
267
  * Auto Compact token watermark `A = floor(C × a)` when the standard-profile
233
268
  * History linkage resolved for this Session; absent for Custom, Off, Native,
@@ -267,6 +302,8 @@ export interface ResolvedConfig {
267
302
  readonly historyKeepRecentToolCalls?: number
268
303
  readonly historyKeepRecentTokens?: number
269
304
  readonly historyMinReclaimTokens?: number
305
+ /** Read-class input cap in characters when configured; absent otherwise. */
306
+ readonly readInputCapChars?: number
270
307
  /**
271
308
  * Auto Compact threshold percent frozen into this deployment by the preset
272
309
  * overlay generation (50-90 integer). Supersedes the live Host setting.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Host-side guard for the advisory advisor's HTTP report route.
3
+ *
4
+ * Pins the read-only contract: the route registers under both prefixes only
5
+ * when the row opts in, a session whose advisor never ran reports nulls and
6
+ * empty arrays (not an error), a populated session reports its decay figure
7
+ * and score distribution content-free, and every error path answers a
8
+ * precise status — 400 for a missing sessionId, 404 for an unknown session,
9
+ * 503 when no agents service can resolve sessions at all.
10
+ */
11
+
12
+ import { Context } from '@deepseek-ai/cordis'
13
+ import { afterEach, describe, expect, it } from 'vitest'
14
+ import { apply } from '../src/index.ts'
15
+ import { getAdvisorState, recordScore, recordRecertified } from '../src/runtime/tokenpilot/advisor-state.ts'
16
+
17
+ const REPORT_ROUTE = '/api/dsh-context-compression-improved/advisor-report'
18
+ const LEGACY_REPORT_ROUTE = '/endpoint/dsh-context-compression-improved/advisor-report'
19
+
20
+ interface RegisteredRoute {
21
+ kind: string
22
+ path: string
23
+ handler: (req: unknown, res: unknown) => unknown
24
+ }
25
+
26
+ interface FakeResponse {
27
+ status?: number
28
+ body?: string
29
+ }
30
+
31
+ let ctx: Context | undefined
32
+
33
+ afterEach(async () => {
34
+ await ctx?.fiber.dispose()
35
+ ctx = undefined
36
+ })
37
+
38
+ const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 20))
39
+
40
+ async function mountWebServer(runtime: Context, routes: RegisteredRoute[]): Promise<void> {
41
+ await runtime.plugin({
42
+ name: 'fake-webserver',
43
+ apply(webCtx) {
44
+ webCtx.provide('webServer', {
45
+ tables: { exact: new Map<string, RegisteredRoute>() },
46
+ register(this: { tables: { exact: Map<string, RegisteredRoute> } }, route: RegisteredRoute) {
47
+ const table = this.tables.exact
48
+ if (table.has(route.path)) {
49
+ throw new Error(`webserver: duplicate ${route.kind} route "${route.path}"`)
50
+ }
51
+ table.set(route.path, route)
52
+ routes.push(route)
53
+ return () => {}
54
+ },
55
+ })
56
+ },
57
+ })
58
+ }
59
+
60
+ /** Mount an agents service that knows session 's1' (and nothing else). */
61
+ function mountAgents(runtime: Context): void {
62
+ void runtime.plugin({
63
+ name: 'fake-agents',
64
+ apply(serviceCtx) {
65
+ serviceCtx.provide('agents', {
66
+ get: (id: unknown) => {
67
+ if (id !== 's1') return undefined
68
+ const session = { id: 's1' }
69
+ const state = getAdvisorState(session as never)
70
+ state.summary = {
71
+ overallTask: 'migrate the auth module',
72
+ activeSubtasks: ['port login flow'],
73
+ keywords: ['auth'],
74
+ todoVersion: 'aaaa1111',
75
+ turn: 4,
76
+ }
77
+ state.lastDecay = { decay: 0.42, weightedChars: 10_000, turn: 4 }
78
+ recordScore(state, 3, { score: 0.95, turn: 4 })
79
+ recordScore(state, 5, { score: 0.10, turn: 4 })
80
+ recordRecertified(state, 5, 4)
81
+ return { session }
82
+ },
83
+ })
84
+ },
85
+ })
86
+ }
87
+
88
+ async function invoke(route: RegisteredRoute, req?: unknown): Promise<FakeResponse> {
89
+ const captured: FakeResponse = {}
90
+ const res = {
91
+ writeHead(code: number) { captured.status = code },
92
+ end(body?: string) { if (body !== undefined) captured.body = body },
93
+ }
94
+ await route.handler(req ?? { method: 'GET' }, res)
95
+ await settle()
96
+ return captured
97
+ }
98
+
99
+ describe('advisor report route registration', () => {
100
+ it('registers both prefixes only when the row opts in', async () => {
101
+ const routes: RegisteredRoute[] = []
102
+ const runtime = new Context()
103
+ ctx = runtime
104
+ await mountWebServer(runtime, routes)
105
+ apply(runtime, { advisorReportRoute: true })
106
+ await settle()
107
+ const paths = routes.map(route => route.path)
108
+ expect(paths).toContain(REPORT_ROUTE)
109
+ expect(paths).toContain(LEGACY_REPORT_ROUTE)
110
+ })
111
+
112
+ it('registers nothing by default (advisor off adds zero behavior)', async () => {
113
+ const routes: RegisteredRoute[] = []
114
+ const runtime = new Context()
115
+ ctx = runtime
116
+ await mountWebServer(runtime, routes)
117
+ apply(runtime, {})
118
+ await settle()
119
+ expect(routes.map(route => route.path)).not.toContain(REPORT_ROUTE)
120
+ })
121
+ })
122
+
123
+ describe('advisor report route responses', () => {
124
+ async function mountedRoute(): Promise<RegisteredRoute> {
125
+ const routes: RegisteredRoute[] = []
126
+ const runtime = new Context()
127
+ ctx = runtime
128
+ await mountWebServer(runtime, routes)
129
+ apply(runtime, { advisorReportRoute: true })
130
+ await settle()
131
+ const route = routes.find(entry => entry.path === REPORT_ROUTE)
132
+ expect(route).toBeDefined()
133
+ return route as RegisteredRoute
134
+ }
135
+
136
+ it('answers 503 without an agents service', async () => {
137
+ const route = await mountedRoute()
138
+ const response = await invoke(route, { url: `${REPORT_ROUTE}?sessionId=s1` })
139
+ expect(response.status).toBe(503)
140
+ })
141
+
142
+ it('answers 404 for an unknown session and 400 for a missing sessionId', async () => {
143
+ const routes: RegisteredRoute[] = []
144
+ const runtime = new Context()
145
+ ctx = runtime
146
+ await mountWebServer(runtime, routes)
147
+ mountAgents(runtime)
148
+ apply(runtime, { advisorReportRoute: true })
149
+ await settle()
150
+ const route = routes.find(entry => entry.path === REPORT_ROUTE) as RegisteredRoute
151
+
152
+ const unknown = await invoke(route, { url: `${REPORT_ROUTE}?sessionId=other` })
153
+ expect(unknown.status).toBe(404)
154
+
155
+ const missing = await invoke(route, { url: REPORT_ROUTE })
156
+ expect(missing.status).toBe(400)
157
+ })
158
+
159
+ it('serves the decay figure, summary, and scores content-free', async () => {
160
+ const routes: RegisteredRoute[] = []
161
+ const runtime = new Context()
162
+ ctx = runtime
163
+ await mountWebServer(runtime, routes)
164
+ mountAgents(runtime)
165
+ apply(runtime, { advisorReportRoute: true })
166
+ await settle()
167
+ const route = routes.find(entry => entry.path === REPORT_ROUTE) as RegisteredRoute
168
+
169
+ const response = await invoke(route, { url: `${REPORT_ROUTE}?sessionId=s1` })
170
+ expect(response.status).toBe(200)
171
+ const body = JSON.parse(response.body ?? '{}') as {
172
+ ok: boolean
173
+ sessionId: string
174
+ advisor: {
175
+ summary: { overallTask: string } | null
176
+ decay: number | null
177
+ weightedChars: number | null
178
+ scores: { seq: number, score: number }[]
179
+ lowRelevanceSeqs: number[]
180
+ }
181
+ }
182
+ expect(body.ok).toBe(true)
183
+ expect(body.sessionId).toBe('s1')
184
+ expect(body.advisor.decay).toBeCloseTo(0.42, 12)
185
+ expect(body.advisor.summary?.overallTask).toBe('migrate the auth module')
186
+ expect(body.advisor.scores).toHaveLength(2)
187
+ expect(body.advisor.lowRelevanceSeqs).toEqual([5])
188
+ // Content-free: no message text ever rides along.
189
+ expect(response.body).not.toContain('"text"')
190
+ expect(response.body).not.toContain('"content"')
191
+ expect(response.body).not.toContain('apiKey')
192
+ })
193
+
194
+ it('answers an empty report (nulls, not an error) for a session whose advisor never ran', async () => {
195
+ const routes: RegisteredRoute[] = []
196
+ const runtime = new Context()
197
+ ctx = runtime
198
+ await mountWebServer(runtime, routes)
199
+ void runtime.plugin({
200
+ name: 'fake-agents-empty',
201
+ apply(serviceCtx) {
202
+ serviceCtx.provide('agents', {
203
+ get: (id: unknown) => (id === 's1' ? { session: { id: 's1' } } : undefined),
204
+ })
205
+ },
206
+ })
207
+ apply(runtime, { advisorReportRoute: true })
208
+ await settle()
209
+ const route = routes.find(entry => entry.path === REPORT_ROUTE) as RegisteredRoute
210
+
211
+ const response = await invoke(route, { url: `${REPORT_ROUTE}?sessionId=s1` })
212
+ expect(response.status).toBe(200)
213
+ const body = JSON.parse(response.body ?? '{}') as {
214
+ ok: boolean
215
+ advisor: { summary: unknown, decay: unknown, scores: unknown[], lowRelevanceSeqs: unknown[] }
216
+ }
217
+ expect(body.ok).toBe(true)
218
+ expect(body.advisor.summary).toBeNull()
219
+ expect(body.advisor.decay).toBeNull()
220
+ expect(body.advisor.scores).toEqual([])
221
+ expect(body.advisor.lowRelevanceSeqs).toEqual([])
222
+ })
223
+ })
@@ -43,10 +43,11 @@ describe('standalone package contract', () => {
43
43
  expect(rootManifest.dsh?.bundle?.patch).toBe('./packages/selector/cordis.patch.yml')
44
44
  expect(rootManifest.dsh?.client?.inject).toEqual([
45
45
  '@deepseek-ai/dsh-client-locale',
46
+ '@deepseek-ai/dsh-client-ui-primitives',
46
47
  '@deepseek-ai/dsh-client-ui-slots',
47
48
  '@deepseek-ai/dsh-client-ui-settings',
48
49
  ])
49
- expect(rootManifest.engines?.dsh).toBe('>=0.1.5-alpha.1 <0.2.0-0')
50
+ expect(rootManifest.engines?.dsh).toBe('>=0.1.5-rc.1 <0.2.0-0')
50
51
  })
51
52
 
52
53
  it('uses the community package in the one Bundle patch', () => {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Production-shaped guard for the review routes' pruner resolution.
3
+ *
4
+ * The older suite provided `toolResultPruner` at the TOP LEVEL, which is a shape
5
+ * production never has: `canonicalCompressionRows()` mounts every pruner inside
6
+ * an agent preset's isolated group, so a top-level `ctx.get('toolResultPruner')`
7
+ * resolves nothing and the queue route answered 503 "review pipeline
8
+ * unavailable" for every request while the pipeline itself ran normally.
9
+ *
10
+ * These cases mount NO top-level service and publish through the review registry
11
+ * from a CHILD fiber instead — the path the fix relies on.
12
+ */
13
+
14
+ import { Context } from '@deepseek-ai/cordis'
15
+ import { afterEach, describe, expect, it } from 'vitest'
16
+ import { apply } from '../src/index.ts'
17
+ import { registerReviewPruner, type ReviewPrunerFace } from '../src/runtime/tokenpilot/review-registry.ts'
18
+
19
+ const QUEUE_ROUTE = '/api/dsh-context-compression-improved/review-queue'
20
+
21
+ interface RegisteredRoute {
22
+ kind: string
23
+ path: string
24
+ handler: (req: unknown, res: unknown) => unknown
25
+ }
26
+
27
+ let ctx: Context | undefined
28
+
29
+ afterEach(async () => {
30
+ await ctx?.fiber.dispose()
31
+ ctx = undefined
32
+ })
33
+
34
+ const settle = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 20))
35
+
36
+ /** A pruner face carrying one pending proposal for session `s1`. */
37
+ const SAMPLE = {
38
+ id: 'abc123def456',
39
+ kind: 'read-state',
40
+ items: [{ seq: 6, kind: 'read-state', component: 'history', tokensBefore: 2401, tokensAfter: 134 }],
41
+ benefit: { recoveredTokens: 400, penaltyTokens: 900, paybackTurns: 2.25 },
42
+ enqueuedTurn: 3,
43
+ lastTurnIndex: 3,
44
+ } as const
45
+
46
+ function samplePruner(): ReviewPrunerFace {
47
+ return {
48
+ listReviewProposals: (session: unknown) => ((session as { id?: string }).id === 's1' ? [SAMPLE] : []),
49
+ listAllReviewProposals: () => [{ sessionId: 's1', proposals: [SAMPLE] }],
50
+ reviewSummary: () => ({ autoApplied: 2, reviewApplied: 1, expired: 3, voided: 0 }),
51
+ decideReviewProposal: (_session: unknown, proposalId: string) => (proposalId === SAMPLE.id
52
+ ? { ok: true }
53
+ : { ok: false, reason: 'unknown-proposal' }),
54
+ }
55
+ }
56
+
57
+ /** Mount the fake web server the routes register against. */
58
+ async function mountWebServer(runtime: Context, routes: RegisteredRoute[]): Promise<void> {
59
+ await runtime.plugin({
60
+ name: 'fake-webserver',
61
+ apply(webCtx) {
62
+ webCtx.provide('webServer', {
63
+ tables: { exact: new Map<string, RegisteredRoute>() },
64
+ register(this: { tables: { exact: Map<string, RegisteredRoute> } }, route: RegisteredRoute) {
65
+ const table = this.tables.exact
66
+ if (table.has(route.path)) throw new Error(`webserver: duplicate ${route.path}`)
67
+ table.set(route.path, route)
68
+ routes.push(route)
69
+ return () => {}
70
+ },
71
+ })
72
+ },
73
+ })
74
+ }
75
+
76
+ /** Boot the host plugin with both routes opted in, as the Bundle row does. */
77
+ async function bootWithRoutes(): Promise<RegisteredRoute[]> {
78
+ const routes: RegisteredRoute[] = []
79
+ ctx = new Context()
80
+ await mountWebServer(ctx, routes)
81
+ apply(ctx, { estimatorCatalogRoute: true, reviewQueueRoute: true })
82
+ await settle()
83
+ return routes
84
+ }
85
+
86
+ async function readQueue(routes: RegisteredRoute[]): Promise<{ status: number, body: string }> {
87
+ const route = routes.find(candidate => candidate.path === QUEUE_ROUTE)
88
+ expect(route, `route ${QUEUE_ROUTE} must be registered`).toBeDefined()
89
+ // `exactOptionalPropertyTypes` forbids assigning an explicit `undefined` to an
90
+ // optional property, so the widened type must spell it out.
91
+ const captured: { status?: number, body?: string | undefined } = {}
92
+ const res = {
93
+ writeHead(code: number) { captured.status = code },
94
+ end(body?: string) { captured.body = body },
95
+ }
96
+ await route!.handler({ url: QUEUE_ROUTE }, res)
97
+ return { status: captured.status ?? 0, body: captured.body ?? '' }
98
+ }
99
+
100
+ describe('review routes resolve a preset-scoped pruner', () => {
101
+ it('answers 503 while no pruner has been published', async () => {
102
+ const routes = await bootWithRoutes()
103
+ const { status, body } = await readQueue(routes)
104
+ expect(status).toBe(503)
105
+ expect(body).toContain('review pipeline unavailable')
106
+ })
107
+
108
+ it('reaches a pruner published from a CHILD fiber with no top-level service', async () => {
109
+ const routes = await bootWithRoutes()
110
+ // Production shape: the pruner is mounted in a preset's isolated group.
111
+ await ctx!.plugin({
112
+ name: 'preset-scoped-pruner',
113
+ apply(childCtx) {
114
+ childCtx.effect(() => registerReviewPruner(samplePruner()), 'test-preset-pruner')
115
+ },
116
+ })
117
+ await settle()
118
+
119
+ const { status, body } = await readQueue(routes)
120
+ expect(status).toBe(200)
121
+ const parsed = JSON.parse(body) as { ok: boolean, total: number, pending: readonly { id: string }[] }
122
+ expect(parsed.ok).toBe(true)
123
+ expect(parsed.total).toBe(1)
124
+ expect(parsed.pending[0]?.id).toBe(SAMPLE.id)
125
+ })
126
+
127
+ it('drops back to 503 once the publishing fiber disposes', async () => {
128
+ const routes = await bootWithRoutes()
129
+ const fork = await ctx!.plugin({
130
+ name: 'preset-scoped-pruner',
131
+ apply(childCtx) {
132
+ childCtx.effect(() => registerReviewPruner(samplePruner()), 'test-preset-pruner')
133
+ },
134
+ })
135
+ await settle()
136
+ expect((await readQueue(routes)).status).toBe(200)
137
+
138
+ await fork.dispose()
139
+ await settle()
140
+ expect((await readQueue(routes)).status).toBe(503)
141
+ })
142
+ })
@@ -31,9 +31,9 @@ describe('Conservative Adaptive interval', () => {
31
31
  expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
32
32
  previousRequestMeasurement: exact(30_500),
33
33
  measuredNodes: [
34
- { seq: 0, count: exact(10_000) },
35
- { seq: 2, count: exact(20_000) },
36
- { seq: 3, count: exact(500) },
34
+ { seq: 0, count: exact(10_000), characterPressure: 40_000 },
35
+ { seq: 2, count: exact(20_000), characterPressure: 80_000 },
36
+ { seq: 3, count: exact(500), characterPressure: 2_000 },
37
37
  ],
38
38
  })).toEqual({
39
39
  kind: 'available',
@@ -57,7 +57,7 @@ describe('Conservative Adaptive interval', () => {
57
57
  estimatorRevision: 'r1',
58
58
  calibration: { sampleCount: 1, conservativeMarginTokens: 200 },
59
59
  }),
60
- measuredNodes: [{ seq: 0, count: exact(10_000) }],
60
+ measuredNodes: [{ seq: 0, count: exact(10_000), characterPressure: 40_000 }],
61
61
  })).toMatchObject({
62
62
  kind: 'available',
63
63
  measurementKind: 'tokenizer-estimate',
@@ -79,7 +79,7 @@ describe('Conservative Adaptive interval', () => {
79
79
  previousPromptTokens: 200,
80
80
  expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
81
81
  previousRequestMeasurement: measurement,
82
- measuredNodes: [{ seq: 0, count: exact(50) }],
82
+ measuredNodes: [{ seq: 0, count: exact(50), characterPressure: 200 }],
83
83
  })).toEqual({ kind: 'unknown', reason })
84
84
  })
85
85
 
@@ -90,7 +90,7 @@ describe('Conservative Adaptive interval', () => {
90
90
  previousPromptTokens: 200,
91
91
  expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
92
92
  previousRequestMeasurement: exact(200, 'DeepSeek-V4-Flash-0731'),
93
- measuredNodes: [{ seq: 0, count: exact(50) }],
93
+ measuredNodes: [{ seq: 0, count: exact(50), characterPressure: 200 }],
94
94
  })).toEqual({ kind: 'unknown', reason: 'request-tokenizer-revision-mismatch' })
95
95
  })
96
96
 
@@ -101,7 +101,7 @@ describe('Conservative Adaptive interval', () => {
101
101
  previousPromptTokens: 200,
102
102
  expectedTokenizerRevision: 'DeepSeek-V4-Pro-0813',
103
103
  previousRequestMeasurement: exact(200),
104
- measuredNodes: [{ seq: 0, count: exact(50, 'DeepSeek-V4-Flash-0731') }],
104
+ measuredNodes: [{ seq: 0, count: exact(50, 'DeepSeek-V4-Flash-0731'), characterPressure: 200 }],
105
105
  })).toEqual({ kind: 'unknown', reason: 'exact-prefix-tokenizer-revision-mismatch' })
106
106
  })
107
107