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,710 +1,463 @@
1
- /** Host owner of the context-compression preference consumed by the browser selector. */
2
-
3
- import type { Context } from '@deepseek-ai/cordis'
4
- import z from '@deepseek-ai/schemastery'
5
- import {
6
- type SettingsScope,
7
- type default as SettingsService,
8
- } from '@deepseek-ai/dsh-settings'
9
- import { buildEstimatorCatalog, type EstimatorCatalogDeps } from './estimator-catalog.ts'
10
- import {
11
- CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
12
- ContextCompressionSettingsSchema,
13
- } from './runtime/config.ts'
14
- import { resolveReviewPruner, type ReviewPrunerFace } from './runtime/tokenpilot/review-registry.ts'
15
- import { getAdvisorState } from './runtime/tokenpilot/advisor-state.ts'
16
-
17
- // The settings namespace literal and the settings schema are owned by the
18
- // runtime config module. Both were once inlined/replaced here to dodge a
19
- // cross-package dependency (ab2175a: the namespace literal was duplicated and
20
- // the schema was downgraded to `z.any()`); the runtime is now part of this
21
- // package, so the real schema is back and the daily Custom defaults are
22
- // published again through settings.register(). The namespace value is
23
- // unchanged ('context-compression').
24
- const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as never
25
- import {
26
- decorateAgentPresets,
27
- resolveCompressionModulePaths,
28
- } from './preset-overlay.ts'
29
-
30
- // 0.1.1/0.1.2 clients ask for /endpoint; 0.1.5 and later ask for /api. Both
31
- // absolute paths are registered (each is its own (kind, path) table entry) and
32
- // one handler serves both prefixes. The bundled client only probes /api.
33
- const ESTIMATOR_CATALOG_ROUTES = [
34
- '/endpoint/dsh-context-compression-improved/estimator-catalog',
35
- '/api/dsh-context-compression-improved/estimator-catalog',
36
- ] as const
37
-
38
- // TokenPilot-inspired R4: the review pipeline's client↔runtime channel, dual
39
- // prefixed like the catalog route so 0.1.2 clients keep working.
40
- const REVIEW_QUEUE_ROUTES = [
41
- '/endpoint/dsh-context-compression-improved/review-queue',
42
- '/api/dsh-context-compression-improved/review-queue',
43
- ] as const
44
- const REVIEW_DECIDE_ROUTES = [
45
- '/endpoint/dsh-context-compression-improved/review-decide',
46
- '/api/dsh-context-compression-improved/review-decide',
47
- ] as const
48
-
49
- // Advisory advisor: one read-only report route, dual prefixed like the others.
50
- const ADVISOR_REPORT_ROUTES = [
51
- '/endpoint/dsh-context-compression-improved/advisor-report',
52
- '/api/dsh-context-compression-improved/advisor-report',
53
- ] as const
54
-
55
- /**
56
- * The review faces of the pruner service the routes consume. Owned by the
57
- * review registry, which the routes also fall back to when no top-level
58
- * `toolResultPruner` service exists — the preset-scoped case in production.
59
- */
60
- type ReviewPrunerLike = ReviewPrunerFace
61
-
62
- /** Minimal face of the agents service: session id → agent (carrying the session). */
63
- interface AgentsServiceLike {
64
- get?(id: unknown): { session?: unknown } | undefined
65
- }
66
-
67
- /**
68
- * Resolve the review pipeline for the top-level routes.
69
- *
70
- * A top-level `toolResultPruner` service wins when a deployment actually mounts
71
- * one, but in production every pruner lives inside an agent preset's isolated
72
- * group, so the registry is the path that resolves. Without the fallback the
73
- * queue route answered 503 "review pipeline unavailable" on every request while
74
- * the review pipeline itself was running normally.
75
- */
76
- function reviewPrunerOf(readService: (name: string) => unknown): ReviewPrunerLike | undefined {
77
- const candidate = readService('toolResultPruner') as {
78
- listReviewProposals?: unknown
79
- decideReviewProposal?: unknown
80
- } | undefined
81
- if (typeof candidate?.listReviewProposals === 'function'
82
- && typeof candidate?.decideReviewProposal === 'function') {
83
- return candidate as unknown as ReviewPrunerLike
84
- }
85
- return resolveReviewPruner()
86
- }
87
-
88
- function sessionFor(readService: (name: string) => unknown, sessionId: string): unknown {
89
- const agents = readService('agents') as AgentsServiceLike | undefined
90
- return typeof agents?.get === 'function' ? agents.get(sessionId)?.session : undefined
91
- }
92
-
93
- function reviewJson(res: unknown, status: number, body: unknown): void {
94
- const resTyped = res as {
95
- writeHead: (code: number, headers?: Record<string, string>) => void
96
- end: (body?: string) => void
97
- }
98
- resTyped.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
99
- resTyped.end(JSON.stringify(body))
100
- }
101
-
102
- function readRequestBody(req: unknown): Promise<string> {
103
- return new Promise((resolve, reject) => {
104
- const typed = req as {
105
- on?: (event: string, listener: (chunk?: Buffer) => void) => void
106
- }
107
- let data = ''
108
- try {
109
- typed.on?.('data', chunk => {
110
- data += String(chunk ?? '')
111
- if (data.length > 64 * 1024) {
112
- data = ''
113
- resolve('')
114
- }
115
- })
116
- typed.on?.('end', () => resolve(data))
117
- typed.on?.('error', reject)
118
- } catch (error) {
119
- reject(error instanceof Error ? error : new Error(String(error)))
120
- }
121
- })
122
- }
123
-
124
- /**
125
- * Serve the review pipeline's two HTTP routes (best effort, mirroring the
126
- * estimator-catalog registration):
127
- *
128
- * - `GET .../review-queue?sessionId=…` → the session's pending proposals with
129
- * their benefit numbers. Sanitized by construction: the queue never holds
130
- * message content, and the response carries ids/seqs/counts only (digests
131
- * stay in the runtime — the client cannot need them).
132
- * - `POST .../review-decide` `{sessionId, proposalId, decision}` → one human
133
- * decision. Invalid body → 400; unknown/not-pending proposal → 404; review
134
- * mode off for the session → 503.
135
- */
136
- function registerReviewQueueRoutes(ctx: Context): void {
137
- const readService = (name: string): unknown => {
138
- try {
139
- return (ctx as unknown as { get: (service: string) => unknown }).get(name)
140
- } catch {
141
- return undefined
142
- }
143
- }
144
- const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
145
- console[level](message, ...args)
146
- }
147
- const registered = (): void => {
148
- log('info', 'context-compression review queue routes registered: %s / %s', REVIEW_QUEUE_ROUTES.join(', '), REVIEW_DECIDE_ROUTES.join(', '))
149
- }
150
-
151
- type SanitizedProposal = {
152
- sessionId: string
153
- id: string
154
- kind: string
155
- items: readonly { seq: number, kind: string, component: string, tokensBefore: number, tokensAfter: number }[]
156
- benefit: {
157
- recoveredTokens: number
158
- penaltyTokens: number
159
- paybackTurns?: number
160
- expectedSaving?: number
161
- }
162
- enqueuedTurn: number
163
- lastTurnIndex: number
164
- }
165
-
166
- const getHandler = (req: unknown, res: unknown): void => {
167
- const pruner = reviewPrunerOf(readService)
168
- if (pruner === undefined || pruner.listAllReviewProposals === undefined) {
169
- reviewJson(res, 503, { ok: false, error: 'review pipeline unavailable' })
170
- return
171
- }
172
- let sessionId = ''
173
- try {
174
- const url = new URL(String((req as { url?: string }).url ?? ''), 'http://localhost')
175
- sessionId = url.searchParams.get('sessionId') ?? ''
176
- } catch {
177
- sessionId = ''
178
- }
179
- const sanitize = (
180
- sid: string,
181
- proposal: {
182
- id: string
183
- kind: string
184
- items: readonly { seq: number, kind: string, component: string, tokensBefore: number, tokensAfter: number }[]
185
- benefit: { recoveredTokens: number, penaltyTokens: number, paybackTurns?: number, expectedSaving?: number }
186
- enqueuedTurn: number
187
- lastTurnIndex: number
188
- },
189
- ): SanitizedProposal => ({
190
- sessionId: sid,
191
- id: proposal.id,
192
- kind: proposal.kind,
193
- items: proposal.items.map(item => ({
194
- seq: item.seq,
195
- kind: item.kind,
196
- component: item.component,
197
- tokensBefore: item.tokensBefore,
198
- tokensAfter: item.tokensAfter,
199
- })),
200
- benefit: {
201
- recoveredTokens: proposal.benefit.recoveredTokens,
202
- penaltyTokens: proposal.benefit.penaltyTokens,
203
- ...(proposal.benefit.paybackTurns === undefined ? {} : { paybackTurns: proposal.benefit.paybackTurns }),
204
- ...(proposal.benefit.expectedSaving === undefined ? {} : { expectedSaving: proposal.benefit.expectedSaving }),
205
- },
206
- enqueuedTurn: proposal.enqueuedTurn,
207
- lastTurnIndex: proposal.lastTurnIndex,
208
- })
209
-
210
- // Without a sessionId the read aggregates every session with live
211
- // proposals — the client carries no session id of its own.
212
- if (sessionId === '') {
213
- const pending = pruner.listAllReviewProposals()
214
- .flatMap(entry => entry.proposals.map(proposal => sanitize(entry.sessionId, proposal)))
215
- reviewJson(res, 200, { ok: true, total: pending.length, pending })
216
- return
217
- }
218
-
219
- const session = sessionFor(readService, sessionId)
220
- if (session === undefined) {
221
- reviewJson(res, 404, { ok: false, error: 'unknown session' })
222
- return
223
- }
224
- const pending = pruner.listReviewProposals(session).map(proposal => sanitize(sessionId, proposal))
225
- const summary = pruner.reviewSummary?.(session)
226
- reviewJson(res, 200, {
227
- ok: true,
228
- sessionId,
229
- total: pending.length,
230
- pending,
231
- ...(summary === undefined ? {} : { summary }),
232
- })
233
- }
234
-
235
- const decideHandler = async (req: unknown, res: unknown): Promise<void> => {
236
- const pruner = reviewPrunerOf(readService)
237
- if (pruner === undefined) {
238
- reviewJson(res, 503, { ok: false, error: 'review pipeline unavailable' })
239
- return
240
- }
241
- let body: unknown
242
- try {
243
- body = JSON.parse(await readRequestBody(req))
244
- } catch {
245
- body = undefined
246
- }
247
- if (typeof body !== 'object' || body === null) {
248
- reviewJson(res, 400, { ok: false, error: 'invalid JSON body' })
249
- return
250
- }
251
- const record = body as { sessionId?: unknown, proposalId?: unknown, decision?: unknown }
252
- if (typeof record.sessionId !== 'string' || record.sessionId === ''
253
- || typeof record.proposalId !== 'string' || record.proposalId === '') {
254
- reviewJson(res, 400, { ok: false, error: 'sessionId and proposalId are required' })
255
- return
256
- }
257
- if (record.decision !== 'approved' && record.decision !== 'rejected' && record.decision !== 'ignored') {
258
- reviewJson(res, 400, { ok: false, error: 'decision must be approved, rejected, or ignored' })
259
- return
260
- }
261
- const session = sessionFor(readService, record.sessionId)
262
- if (session === undefined) {
263
- reviewJson(res, 404, { ok: false, error: 'unknown session' })
264
- return
265
- }
266
- const outcome = pruner.decideReviewProposal(session, record.proposalId, record.decision)
267
- if (outcome === undefined) {
268
- reviewJson(res, 503, { ok: false, error: 'review mode is off for this session' })
269
- return
270
- }
271
- if (!outcome.ok) {
272
- reviewJson(res, 404, { ok: false, error: outcome.reason })
273
- return
274
- }
275
- reviewJson(res, 200, { ok: true, sessionId: record.sessionId, proposalId: record.proposalId, decision: record.decision })
276
- }
277
-
278
- const register = (webServer: WebServerLike): void => {
279
- const table: ReadonlyArray<{ path: string, handler: (req: unknown, res: unknown) => unknown }> = [
280
- ...[...REVIEW_QUEUE_ROUTES].map(path => ({ path, handler: getHandler })),
281
- ...[...REVIEW_DECIDE_ROUTES].map(path => ({ path, handler: (req: unknown, res: unknown) => { void decideHandler(req, res) } })),
282
- ]
283
- const disposers = table
284
- .map(entry => webServer.register({ kind: 'exact', path: entry.path, handler: entry.handler }))
285
- .filter((off): off is () => void => typeof off === 'function')
286
- ctx.effect(
287
- () => () => { for (const off of disposers) off() },
288
- 'contextCompressionSelector.review routes',
289
- )
290
- registered()
291
- }
292
-
293
- const active = asWebServer(readService('webServer'))
294
- if (active !== undefined) {
295
- register(active)
296
- return
297
- }
298
-
299
- ctx.inject(['webServer'], (injected) => {
300
- const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
301
- if (webServer === undefined) {
302
- log('warn', 'context-compression webServer exposes no register() review routes not registered')
303
- return
304
- }
305
- register(webServer)
306
- })
307
-
308
- log('warn', 'context-compression webServer not active yet — review routes pending: %s', REVIEW_QUEUE_ROUTES.join(', '))
309
- }
310
-
311
- /**
312
- * Serve the advisory advisor's read-only report route (same registration
313
- * skeleton as the review routes):
314
- *
315
- * `GET .../advisor-report?sessionId=…` → the session's prefix-decay figure,
316
- * the todolist-bound task summary, and the score distribution. Content-free
317
- * by construction: task semantics are LLM-derived summaries, never message
318
- * text, and no score reason or candidate preview is ever returned.
319
- * Unknown session → 404; no agents service → 503. A session whose advisor
320
- * never ran reports nulls and empty arrays, not an error.
321
- */
322
- function registerAdvisorReportRoute(ctx: Context): void {
323
- const readService = (name: string): unknown => {
324
- try {
325
- return (ctx as unknown as { get: (service: string) => unknown }).get(name)
326
- } catch {
327
- return undefined
328
- }
329
- }
330
- const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
331
- console[level](message, ...args)
332
- }
333
-
334
- const getHandler = (req: unknown, res: unknown): void => {
335
- const agents = readService('agents') as AgentsServiceLike | undefined
336
- if (typeof agents?.get !== 'function') {
337
- reviewJson(res, 503, { ok: false, error: 'advisor report unavailable' })
338
- return
339
- }
340
- let sessionId = ''
341
- try {
342
- const url = new URL(String((req as { url?: string }).url ?? ''), 'http://localhost')
343
- sessionId = url.searchParams.get('sessionId') ?? ''
344
- } catch {
345
- // Malformed URL: fall through with the empty sessionId already set.
346
- }
347
- if (sessionId === '') {
348
- reviewJson(res, 400, { ok: false, error: 'sessionId is required' })
349
- return
350
- }
351
- const session = sessionFor(readService, sessionId)
352
- if (session === undefined) {
353
- reviewJson(res, 404, { ok: false, error: 'unknown session' })
354
- return
355
- }
356
- const state = getAdvisorState(session as Parameters<typeof getAdvisorState>[0])
357
- reviewJson(res, 200, {
358
- ok: true,
359
- sessionId,
360
- advisor: {
361
- summary: state.summary ?? null,
362
- decay: state.lastDecay?.decay ?? null,
363
- weightedChars: state.lastDecay?.weightedChars ?? null,
364
- decayTurn: state.lastDecay?.turn ?? null,
365
- scores: [...state.scores].map(([seq, entry]) => ({ seq, score: entry.score, turn: entry.turn })),
366
- lowRelevanceSeqs: [...state.recertified.keys()],
367
- },
368
- })
369
- }
370
-
371
- const register = (webServer: WebServerLike): void => {
372
- const table = [...ADVISOR_REPORT_ROUTES].map(path => ({ path, handler: getHandler }))
373
- const disposers = table
374
- .map(entry => webServer.register({ kind: 'exact', path: entry.path, handler: entry.handler }))
375
- .filter((off): off is () => void => typeof off === 'function')
376
- ctx.effect(
377
- () => () => { for (const off of disposers) off() },
378
- 'contextCompressionSelector.advisor report route',
379
- )
380
- log('info', 'context-compression advisor report route registered: %s', ADVISOR_REPORT_ROUTES.join(', '))
381
- }
382
-
383
- const active = asWebServer(readService('webServer'))
384
- if (active !== undefined) {
385
- register(active)
386
- return
387
- }
388
-
389
- ctx.inject(['webServer'], (injected) => {
390
- const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
391
- if (webServer === undefined) {
392
- log('warn', 'context-compression webServer exposes no register() — advisor report route not registered')
393
- return
394
- }
395
- register(webServer)
396
- })
397
-
398
- log('warn', 'context-compression webServer not active yet advisor report route pending: %s', ADVISOR_REPORT_ROUTES.join(', '))
399
- }
400
-
401
- /**
402
- * The one service the catalog route actually needs. `llm` and
403
- * `agentDefaultModel` are payload enrichment the handler resolves per request,
404
- * never reasons to withhold the route.
405
- */
406
- const ESTIMATOR_CATALOG_ROUTE_DEPS: readonly ['webServer'] = ['webServer']
407
-
408
- /** The minimal face of the DSH `webServer` service this plugin uses. */
409
- interface WebServerLike {
410
- register(route: {
411
- kind: 'exact'
412
- path: string
413
- handler: (req: unknown, res: unknown) => unknown
414
- }): () => void
415
- }
416
-
417
- /** The estimator-side service the catalog handler enriches its response with. */
418
- interface AgentDefaultModelLike {
419
- /** Current host model-group selection, when the service exposes one. */
420
- currentSelection?: () => { provider?: unknown, model?: unknown } | undefined
421
- }
422
-
423
- function asWebServer(value: unknown): WebServerLike | undefined {
424
- if (typeof value !== 'object' || value === null) return undefined
425
- const candidate = value as { register?: unknown }
426
- // Hand back the service itself -- never a wrapper re-exporting `register`.
427
- // The host reads its route tables off `this` (`this.exact` / `this.prefixes`),
428
- // so a detached call makes `this` the wrapper and throws "Cannot read
429
- // properties of undefined (reading 'has')" inside the host, after which the
430
- // route is simply absent and the client sees a bare 404. The 0.1.5 branch
431
- // never had that defect; take this implementation, not the 0.1.2 history's.
432
- return typeof candidate.register === 'function' ? (value as WebServerLike) : undefined
433
- }
434
-
435
- /**
436
- * Serve `GET /api/dsh-context-compression-improved/estimator-catalog` — the
437
- * settings card's host-route dropdowns (live provider/model groups from the DSH
438
- * `llm` service plus the effective selection). Best effort: without the
439
- * `webServer` service the plugin keeps working, only the HTTP API is missing.
440
- *
441
- * The route gates on `webServer` **alone**. `llm` and `agentDefaultModel` only
442
- * enrich the response and are resolved per request, so listing them here would
443
- * let an estimator-side service the handler never needs keep the route
444
- * unregistered. That failure is silent by construction — an unsatisfied
445
- * `ctx.inject` callback never runs, so the plugin simply has no HTTP API and
446
- * every request falls through to the host 404.
447
- *
448
- * Two channels cover the two arrival orders: a direct lookup catches a
449
- * `webServer` that is already active when the plugin loads, and `ctx.inject`
450
- * catches one that activates later. Both funnel into a single guarded
451
- * registration, because a late-arriving service must not re-register a
452
- * `(kind, path)` the host treats as a composition-contract violation.
453
- */
454
- function registerEstimatorCatalogRoute(ctx: Context): void {
455
- const readService = (name: string): unknown => {
456
- try {
457
- return (ctx as unknown as { get: (service: string) => unknown }).get(name)
458
- } catch {
459
- return undefined
460
- }
461
- }
462
- // `console`, not `ctx.logger`: measured on the 0.1.2 host, the cordis logger
463
- // surfaces no plugin output in the `dsh web` terminal at all — a full boot
464
- // produced zero plugin log lines while the process itself stayed chatty — so
465
- // a lifecycle diagnostic published there is unobservable. `dsh-perm-gate`
466
- // uses `console.warn` for the same message class on the same host.
467
- const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
468
- console[level](message, ...args)
469
- }
470
-
471
- let registered = false
472
- const register = (webServer: WebServerLike, channel: 'direct' | 'inject'): void => {
473
- if (registered) return
474
- const handler = (_req: unknown, res: unknown): void => {
475
- const resTyped = res as {
476
- writeHead: (code: number, headers?: Record<string, string>) => void
477
- end: (body?: string) => void
478
- }
479
- if (typeof resTyped?.writeHead !== 'function' || typeof resTyped?.end !== 'function') return
480
- const llm = readService('llm')
481
- const defaults = readService('agentDefaultModel') as AgentDefaultModelLike | undefined
482
- const deps: EstimatorCatalogDeps = {
483
- ...(llm === undefined
484
- ? {}
485
- : { llm: llm as NonNullable<EstimatorCatalogDeps['llm']> }),
486
- ...(typeof defaults?.currentSelection === 'function'
487
- ? { currentSelection: () => defaults.currentSelection?.() }
488
- : {}),
489
- }
490
- buildEstimatorCatalog(deps).then(
491
- catalog => {
492
- resTyped.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
493
- resTyped.end(JSON.stringify({ ok: true, ...catalog }))
494
- },
495
- (error: unknown) => {
496
- resTyped.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
497
- resTyped.end(JSON.stringify({ ok: false, error: String((error as Error)?.message ?? error) }))
498
- },
499
- )
500
- }
501
- try {
502
- const disposers = ESTIMATOR_CATALOG_ROUTES
503
- .map(path => webServer.register({ kind: 'exact', path, handler }))
504
- .filter((off): off is () => void => typeof off === 'function')
505
- registered = true
506
- ctx.effect(
507
- () => () => { for (const off of disposers) off() },
508
- 'contextCompressionSelector.estimator-catalog route',
509
- )
510
- log('info', 'context-compression estimator catalog route registered (%s): %s', channel, ESTIMATOR_CATALOG_ROUTES.join(', '))
511
- } catch (error) {
512
- log('warn', 'context-compression estimator catalog route registration failed (%s): %o', channel, error)
513
- }
514
- }
515
-
516
- const active = asWebServer(readService('webServer'))
517
- if (active !== undefined) {
518
- register(active, 'direct')
519
- if (registered) return
520
- }
521
-
522
- ctx.inject([...ESTIMATOR_CATALOG_ROUTE_DEPS], (injected) => {
523
- const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
524
- if (webServer === undefined) {
525
- log('warn', 'context-compression webServer exposes no register() — estimator catalog route not registered')
526
- return
527
- }
528
- register(webServer, 'inject')
529
- })
530
-
531
- log('warn', 'context-compression webServer not active yet — estimator catalog route pending: %s', ESTIMATOR_CATALOG_ROUTES.join(', '))
532
- }
533
-
534
-
535
-
536
- /** Shared state forwarded through every Cordis proxy of one settings service. */
537
- interface SharedSettingsRegistration {
538
- /** Plugin fibers currently leasing the namespace. */
539
- readonly owners: Set<SettingsOwner>
540
- /** Owner whose fiber currently carries settings.register's native effect. */
541
- registrationOwner: SettingsOwner
542
- /** Current owner scope; replaced without changing the stored document. */
543
- scope: SettingsScope<unknown>
544
- }
545
-
546
- /** One selector Host row able to own the registration effect. */
547
- interface SettingsOwner {
548
- /** Traceable service proxy binding register() to this row's fiber. */
549
- readonly settings: SettingsService
550
- }
551
-
552
- /** Symbol properties reach the shared service target through Cordis proxies. */
553
- const SHARED_SETTINGS = Symbol.for(
554
- 'dsh-context-compression-improved/settings-registration',
555
- )
556
-
557
- type SettingsCarrier = SettingsService & {
558
- [SHARED_SETTINGS]?: SharedSettingsRegistration
559
- }
560
-
561
- /** Standalone Bundle behavior; the settings/UI owner remains safe when false. */
562
- export interface Config {
563
- /** Add the canonical compression stack to every non-Minimal preset. */
564
- presetOverlay?: boolean
565
- /**
566
- * Register the estimator-catalog HTTP route on this row.
567
- *
568
- * The standalone Bundle patch sets this on its own row (which declares
569
- * `inject: [webServer]`), so profiles without a host web server never mount a
570
- * row that could only announce a pending route. The row-level `inject` is
571
- * belt-and-braces: `dsh-host-webserver.register` performs no authorization
572
- * check and `ctx.get(name)` only asks whether the providing fiber is active,
573
- * so the two-channel registration inside this function is what actually
574
- * covers both arrival orders.
575
- */
576
- estimatorCatalogRoute?: boolean
577
- /**
578
- * Register the review pipeline's HTTP routes (pending-queue read + decide
579
- * write) on this row. Same Bundle opt-in semantics as
580
- * `estimatorCatalogRoute`; without the routes the floating window has no
581
- * transport and simply never appears.
582
- */
583
- reviewQueueRoute?: boolean
584
- /**
585
- * Register the advisory advisor's read-only HTTP report route (decay
586
- * figure, task summary, score distribution). Same Bundle opt-in semantics
587
- * as `reviewQueueRoute`; the advisor itself stays off until the user turns
588
- * it on through the `presetOptions.advisor*` settings keys.
589
- */
590
- advisorReportRoute?: boolean
591
- }
592
-
593
- /** Loader validation for the standalone Bundle opt-in. */
594
- export const Config: z<Config> = z.object({
595
- presetOverlay: z.boolean().default(false),
596
- estimatorCatalogRoute: z.boolean().default(false),
597
- reviewQueueRoute: z.boolean().default(false),
598
- advisorReportRoute: z.boolean().default(false),
599
- })
600
-
601
- /** Register the persisted default read by the currently mounted root pruner. */
602
- export function apply(ctx: Context, config: Config = {}): void {
603
- // Measured on the 0.1.2 host: a plugin-load failure surfaces only through the
604
- // cordis logger, which prints nothing in the `dsh web` terminal — so a throw
605
- // here is completely invisible and looks exactly like a plugin that loaded
606
- // and quietly did nothing. Report it to a sink the host shows, then re-throw
607
- // unchanged: behaviour is untouched, only observability is restored.
608
- try {
609
- ctx.inject(['settings'], (settingsCtx) => {
610
- acquireSettingsRegistration(settingsCtx)
611
- })
612
-
613
- // The settings card's host-route dropdowns read this route; it lives on the
614
- // top-level plugin fiber, not inside the isolated `toolResultPruner`
615
- // service, because the route is host-wide rather than per-pruner-instance.
616
- if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
617
-
618
- // TokenPilot-inspired R4: the review pipeline's client transport (see the
619
- // config JSDoc for the opt-in semantics).
620
- if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx)
621
-
622
- // Advisory advisor: read-only decay/score report (opt-in, like review).
623
- if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx)
624
-
625
- if (config.presetOverlay !== true) return
626
-
627
- ctx.inject(['agentPresets'], (presetsCtx) => {
628
- const installation = decorateAgentPresets(
629
- presetsCtx.agentPresets,
630
- {
631
- modules: resolveCompressionModulePaths(),
632
- excludedPresetIds: ['minimal'],
633
- autoCompactThresholdPercent: () => resolveAutoCompactThresholdPercent(presetsCtx),
634
- },
635
- )
636
- presetsCtx.effect(() => () => installation.dispose(), 'contextCompressionSelector.agentPresets()')
637
- })
638
- } catch (error) {
639
- console.error('context-compression selector apply failed: %o', error)
640
- throw error
641
- }
642
- }
643
-
644
- /**
645
- * Read the current Auto Compact threshold ratio at composition time. Settings
646
- * values are revalidated here, and any unreadable value falls back to the 80%
647
- * default rather than blocking preset composition.
648
- */
649
- function resolveAutoCompactThresholdPercent(presetsCtx: Context): number {
650
- const raw = presetsCtx.get('settings')?.get(CONTEXT_COMPRESSION_NAMESPACE)
651
- try {
652
- const record = structuredClone(raw) as Record<string, unknown> | undefined
653
- const threshold = record?.autoCompact as { thresholdPercent?: number } | undefined
654
- const value = typeof threshold?.thresholdPercent === 'number' ? threshold.thresholdPercent : 80
655
- return Number.isFinite(value) && value >= 50 && value <= 90 ? value : 80
656
- } catch {
657
- return 80
658
- }
659
- }
660
-
661
- /**
662
- * Lease one native settings registration across duplicate Host rows.
663
- *
664
- * The lease effect is intentionally registered before settings.register().
665
- * Cordis disposes effects in reverse order, so the native registration first
666
- * releases the namespace; this disposer can then transfer it to another live
667
- * owner without a duplicate-registration window.
668
- */
669
- function acquireSettingsRegistration(ctx: Context): void {
670
- const settings = ctx.settings as SettingsCarrier
671
- const owner: SettingsOwner = { settings }
672
- let shared = settings[SHARED_SETTINGS]
673
- if (shared === undefined) {
674
- shared = {
675
- owners: new Set(),
676
- registrationOwner: owner,
677
- scope: undefined as unknown as SettingsScope<unknown>,
678
- }
679
- Object.defineProperty(settings, SHARED_SETTINGS, {
680
- configurable: true,
681
- enumerable: false,
682
- writable: false,
683
- value: shared,
684
- })
685
- }
686
- shared.owners.add(owner)
687
- const state = shared
688
-
689
- ctx.effect(() => () => {
690
- state.owners.delete(owner)
691
- if (state.registrationOwner === owner && state.owners.size > 0) {
692
- const next = state.owners.values().next().value as SettingsOwner
693
- state.registrationOwner = next
694
- state.scope = next.settings.register(
695
- CONTEXT_COMPRESSION_NAMESPACE,
696
- ContextCompressionSettingsSchema,
697
- )
698
- }
699
- if (state.owners.size === 0 && settings[SHARED_SETTINGS] === state) {
700
- Reflect.deleteProperty(settings, SHARED_SETTINGS)
701
- }
702
- }, 'contextCompressionSelector.settingsLease()')
703
-
704
- if (state.owners.size === 1) {
705
- state.scope = settings.register(
706
- CONTEXT_COMPRESSION_NAMESPACE,
707
- ContextCompressionSettingsSchema,
708
- )
709
- }
710
- }
1
+ /** Host owner of the context-compression preference consumed by the browser selector. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import z from '@deepseek-ai/schemastery'
5
+ import {
6
+ type SettingsScope,
7
+ type default as SettingsService,
8
+ } from '@deepseek-ai/dsh-settings'
9
+ import { buildEstimatorCatalog, type EstimatorCatalogDeps } from './estimator-catalog.ts'
10
+ import {
11
+ CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
12
+ ContextCompressionSettingsSchema,
13
+ } from './runtime/config.ts'
14
+
15
+ import { getAdvisorState } from './runtime/tokenpilot/advisor-state.ts'
16
+
17
+ // The settings namespace literal and the settings schema are owned by the
18
+ // runtime config module. Both were once inlined/replaced here to dodge a
19
+ // cross-package dependency (ab2175a: the namespace literal was duplicated and
20
+ // the schema was downgraded to `z.any()`); the runtime is now part of this
21
+ // package, so the real schema is back and the daily Custom defaults are
22
+ // published again through settings.register(). The namespace value is
23
+ // unchanged ('context-compression').
24
+ const CONTEXT_COMPRESSION_NAMESPACE = CONTEXT_COMPRESSION_SETTINGS_NAMESPACE as never
25
+ import {
26
+ decorateAgentPresets,
27
+ resolveCompressionModulePaths,
28
+ } from './preset-overlay.ts'
29
+
30
+ // 0.1.1/0.1.2 clients ask for /endpoint; 0.1.5 and later ask for /api. Both
31
+ // absolute paths are registered (each is its own (kind, path) table entry) and
32
+ // one handler serves both prefixes. The bundled client only probes /api.
33
+ const ESTIMATOR_CATALOG_ROUTES = [
34
+ '/endpoint/dsh-context-compression-improved/estimator-catalog',
35
+ '/api/dsh-context-compression-improved/estimator-catalog',
36
+ ] as const
37
+
38
+ // Advisory advisor: one read-only report route, dual prefixed like the others.
39
+ const ADVISOR_REPORT_ROUTES = [
40
+ '/endpoint/dsh-context-compression-improved/advisor-report',
41
+ '/api/dsh-context-compression-improved/advisor-report',
42
+ ] as const
43
+
44
+ /** Minimal face of the agents service: session id → agent (carrying the session). */
45
+ interface AgentsServiceLike {
46
+ get?(id: unknown): { session?: unknown } | undefined
47
+ }
48
+
49
+ function sessionFor(readService: (name: string) => unknown, sessionId: string): unknown {
50
+ const agents = readService('agents') as AgentsServiceLike | undefined
51
+ return typeof agents?.get === 'function' ? agents.get(sessionId)?.session : undefined
52
+ }
53
+
54
+ function jsonResponse(res: unknown, status: number, body: unknown): void {
55
+ const resTyped = res as {
56
+ writeHead: (code: number, headers?: Record<string, string>) => void
57
+ end: (body?: string) => void
58
+ }
59
+ resTyped.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
60
+ resTyped.end(JSON.stringify(body))
61
+ }
62
+
63
+ /**
64
+ * Serve the advisory advisor's read-only report route (same registration
65
+ * skeleton as the estimator-catalog route):
66
+ *
67
+ * `GET .../advisor-report?sessionId=…` → the session's prefix-decay figure,
68
+ * the todolist-bound task summary, and the score distribution. Content-free
69
+ * by construction: task semantics are LLM-derived summaries, never message
70
+ * text, and no score reason or candidate preview is ever returned.
71
+ * Unknown session 404; no agents service 503. A session whose advisor
72
+ * never ran reports nulls and empty arrays, not an error.
73
+ */
74
+ function registerAdvisorReportRoute(ctx: Context): void {
75
+ const readService = (name: string): unknown => {
76
+ try {
77
+ return (ctx as unknown as { get: (service: string) => unknown }).get(name)
78
+ } catch {
79
+ return undefined
80
+ }
81
+ }
82
+ const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
83
+ console[level](message, ...args)
84
+ }
85
+
86
+ const getHandler = (req: unknown, res: unknown): void => {
87
+ const agents = readService('agents') as AgentsServiceLike | undefined
88
+ if (typeof agents?.get !== 'function') {
89
+ jsonResponse(res, 503, { ok: false, error: 'advisor report unavailable' })
90
+ return
91
+ }
92
+ let sessionId = ''
93
+ try {
94
+ const url = new URL(String((req as { url?: string }).url ?? ''), 'http://localhost')
95
+ sessionId = url.searchParams.get('sessionId') ?? ''
96
+ } catch {
97
+ // Malformed URL: fall through with the empty sessionId already set.
98
+ }
99
+ if (sessionId === '') {
100
+ jsonResponse(res, 400, { ok: false, error: 'sessionId is required' })
101
+ return
102
+ }
103
+ const session = sessionFor(readService, sessionId)
104
+ if (session === undefined) {
105
+ jsonResponse(res, 404, { ok: false, error: 'unknown session' })
106
+ return
107
+ }
108
+ const state = getAdvisorState(session as Parameters<typeof getAdvisorState>[0])
109
+ jsonResponse(res, 200, {
110
+ ok: true,
111
+ sessionId,
112
+ advisor: {
113
+ summary: state.summary ?? null,
114
+ decay: state.lastDecay?.decay ?? null,
115
+ weightedChars: state.lastDecay?.weightedChars ?? null,
116
+ decayTurn: state.lastDecay?.turn ?? null,
117
+ scores: [...state.scores].map(([seq, entry]) => ({ seq, score: entry.score, turn: entry.turn })),
118
+ lowRelevanceSeqs: [...state.recertified.keys()],
119
+ // The benefit model's label of the last landed batch — advice, never a
120
+ // gate: the batch it describes landed regardless of the band.
121
+ lastAdvice: state.lastAdvice === undefined
122
+ ? null
123
+ : {
124
+ band: state.lastAdvice.band,
125
+ turn: state.lastAdvice.turn,
126
+ itemSeqs: state.lastAdvice.itemSeqs,
127
+ recoveredTokens: state.lastAdvice.recoveredTokens,
128
+ penaltyTokens: state.lastAdvice.penaltyTokens,
129
+ paybackTurns: state.lastAdvice.paybackTurns ?? null,
130
+ },
131
+ },
132
+ })
133
+ }
134
+
135
+ const register = (webServer: WebServerLike): void => {
136
+ const table = [...ADVISOR_REPORT_ROUTES].map(path => ({ path, handler: getHandler }))
137
+ const disposers = table
138
+ .map(entry => webServer.register({ kind: 'exact', path: entry.path, handler: entry.handler }))
139
+ .filter((off): off is () => void => typeof off === 'function')
140
+ ctx.effect(
141
+ () => () => { for (const off of disposers) off() },
142
+ 'contextCompressionSelector.advisor report route',
143
+ )
144
+ log('info', 'context-compression advisor report route registered: %s', ADVISOR_REPORT_ROUTES.join(', '))
145
+ }
146
+
147
+ const active = asWebServer(readService('webServer'))
148
+ if (active !== undefined) {
149
+ register(active)
150
+ return
151
+ }
152
+
153
+ ctx.inject(['webServer'], (injected) => {
154
+ const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
155
+ if (webServer === undefined) {
156
+ log('warn', 'context-compression webServer exposes no register() — advisor report route not registered')
157
+ return
158
+ }
159
+ register(webServer)
160
+ })
161
+
162
+ log('warn', 'context-compression webServer not active yet — advisor report route pending: %s', ADVISOR_REPORT_ROUTES.join(', '))
163
+ }
164
+
165
+ /**
166
+ * The one service the catalog route actually needs. `llm` and
167
+ * `agentDefaultModel` are payload enrichment the handler resolves per request,
168
+ * never reasons to withhold the route.
169
+ */
170
+ const ESTIMATOR_CATALOG_ROUTE_DEPS: readonly ['webServer'] = ['webServer']
171
+
172
+ /** The minimal face of the DSH `webServer` service this plugin uses. */
173
+ interface WebServerLike {
174
+ register(route: {
175
+ kind: 'exact'
176
+ path: string
177
+ handler: (req: unknown, res: unknown) => unknown
178
+ }): () => void
179
+ }
180
+
181
+ /** The estimator-side service the catalog handler enriches its response with. */
182
+ interface AgentDefaultModelLike {
183
+ /** Current host model-group selection, when the service exposes one. */
184
+ currentSelection?: () => { provider?: unknown, model?: unknown } | undefined
185
+ }
186
+
187
+ function asWebServer(value: unknown): WebServerLike | undefined {
188
+ if (typeof value !== 'object' || value === null) return undefined
189
+ const candidate = value as { register?: unknown }
190
+ // Hand back the service itself -- never a wrapper re-exporting `register`.
191
+ // The host reads its route tables off `this` (`this.exact` / `this.prefixes`),
192
+ // so a detached call makes `this` the wrapper and throws "Cannot read
193
+ // properties of undefined (reading 'has')" inside the host, after which the
194
+ // route is simply absent and the client sees a bare 404. The 0.1.5 branch
195
+ // never had that defect; take this implementation, not the 0.1.2 history's.
196
+ return typeof candidate.register === 'function' ? (value as WebServerLike) : undefined
197
+ }
198
+
199
+ /**
200
+ * Serve `GET /api/dsh-context-compression-improved/estimator-catalog` — the
201
+ * settings card's host-route dropdowns (live provider/model groups from the DSH
202
+ * `llm` service plus the effective selection). Best effort: without the
203
+ * `webServer` service the plugin keeps working, only the HTTP API is missing.
204
+ *
205
+ * The route gates on `webServer` **alone**. `llm` and `agentDefaultModel` only
206
+ * enrich the response and are resolved per request, so listing them here would
207
+ * let an estimator-side service the handler never needs keep the route
208
+ * unregistered. That failure is silent by construction — an unsatisfied
209
+ * `ctx.inject` callback never runs, so the plugin simply has no HTTP API and
210
+ * every request falls through to the host 404.
211
+ *
212
+ * Two channels cover the two arrival orders: a direct lookup catches a
213
+ * `webServer` that is already active when the plugin loads, and `ctx.inject`
214
+ * catches one that activates later. Both funnel into a single guarded
215
+ * registration, because a late-arriving service must not re-register a
216
+ * `(kind, path)` the host treats as a composition-contract violation.
217
+ */
218
+ function registerEstimatorCatalogRoute(ctx: Context): void {
219
+ const readService = (name: string): unknown => {
220
+ try {
221
+ return (ctx as unknown as { get: (service: string) => unknown }).get(name)
222
+ } catch {
223
+ return undefined
224
+ }
225
+ }
226
+ // `console`, not `ctx.logger`: measured on the 0.1.2 host, the cordis logger
227
+ // surfaces no plugin output in the `dsh web` terminal at all — a full boot
228
+ // produced zero plugin log lines while the process itself stayed chatty — so
229
+ // a lifecycle diagnostic published there is unobservable. `dsh-perm-gate`
230
+ // uses `console.warn` for the same message class on the same host.
231
+ const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
232
+ console[level](message, ...args)
233
+ }
234
+
235
+ let registered = false
236
+ const register = (webServer: WebServerLike, channel: 'direct' | 'inject'): void => {
237
+ if (registered) return
238
+ const handler = (_req: unknown, res: unknown): void => {
239
+ const resTyped = res as {
240
+ writeHead: (code: number, headers?: Record<string, string>) => void
241
+ end: (body?: string) => void
242
+ }
243
+ if (typeof resTyped?.writeHead !== 'function' || typeof resTyped?.end !== 'function') return
244
+ const llm = readService('llm')
245
+ const defaults = readService('agentDefaultModel') as AgentDefaultModelLike | undefined
246
+ const deps: EstimatorCatalogDeps = {
247
+ ...(llm === undefined
248
+ ? {}
249
+ : { llm: llm as NonNullable<EstimatorCatalogDeps['llm']> }),
250
+ ...(typeof defaults?.currentSelection === 'function'
251
+ ? { currentSelection: () => defaults.currentSelection?.() }
252
+ : {}),
253
+ }
254
+ buildEstimatorCatalog(deps).then(
255
+ catalog => {
256
+ resTyped.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
257
+ resTyped.end(JSON.stringify({ ok: true, ...catalog }))
258
+ },
259
+ (error: unknown) => {
260
+ resTyped.writeHead(500, { 'content-type': 'application/json; charset=utf-8' })
261
+ resTyped.end(JSON.stringify({ ok: false, error: String((error as Error)?.message ?? error) }))
262
+ },
263
+ )
264
+ }
265
+ try {
266
+ const disposers = ESTIMATOR_CATALOG_ROUTES
267
+ .map(path => webServer.register({ kind: 'exact', path, handler }))
268
+ .filter((off): off is () => void => typeof off === 'function')
269
+ registered = true
270
+ ctx.effect(
271
+ () => () => { for (const off of disposers) off() },
272
+ 'contextCompressionSelector.estimator-catalog route',
273
+ )
274
+ log('info', 'context-compression estimator catalog route registered (%s): %s', channel, ESTIMATOR_CATALOG_ROUTES.join(', '))
275
+ } catch (error) {
276
+ log('warn', 'context-compression estimator catalog route registration failed (%s): %o', channel, error)
277
+ }
278
+ }
279
+
280
+ const active = asWebServer(readService('webServer'))
281
+ if (active !== undefined) {
282
+ register(active, 'direct')
283
+ if (registered) return
284
+ }
285
+
286
+ ctx.inject([...ESTIMATOR_CATALOG_ROUTE_DEPS], (injected) => {
287
+ const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
288
+ if (webServer === undefined) {
289
+ log('warn', 'context-compression webServer exposes no register() — estimator catalog route not registered')
290
+ return
291
+ }
292
+ register(webServer, 'inject')
293
+ })
294
+
295
+ log('warn', 'context-compression webServer not active yet — estimator catalog route pending: %s', ESTIMATOR_CATALOG_ROUTES.join(', '))
296
+ }
297
+
298
+
299
+
300
+ /** Shared state forwarded through every Cordis proxy of one settings service. */
301
+ interface SharedSettingsRegistration {
302
+ /** Plugin fibers currently leasing the namespace. */
303
+ readonly owners: Set<SettingsOwner>
304
+ /** Owner whose fiber currently carries settings.register's native effect. */
305
+ registrationOwner: SettingsOwner
306
+ /** Current owner scope; replaced without changing the stored document. */
307
+ scope: SettingsScope<unknown>
308
+ }
309
+
310
+ /** One selector Host row able to own the registration effect. */
311
+ interface SettingsOwner {
312
+ /** Traceable service proxy binding register() to this row's fiber. */
313
+ readonly settings: SettingsService
314
+ }
315
+
316
+ /** Symbol properties reach the shared service target through Cordis proxies. */
317
+ const SHARED_SETTINGS = Symbol.for(
318
+ 'dsh-context-compression-improved/settings-registration',
319
+ )
320
+
321
+ type SettingsCarrier = SettingsService & {
322
+ [SHARED_SETTINGS]?: SharedSettingsRegistration
323
+ }
324
+
325
+ /** Standalone Bundle behavior; the settings/UI owner remains safe when false. */
326
+ export interface Config {
327
+ /** Add the canonical compression stack to every non-Minimal preset. */
328
+ presetOverlay?: boolean
329
+ /**
330
+ * Register the estimator-catalog HTTP route on this row.
331
+ *
332
+ * The standalone Bundle patch sets this on its own row (which declares
333
+ * `inject: [webServer]`), so profiles without a host web server never mount a
334
+ * row that could only announce a pending route. The row-level `inject` is
335
+ * belt-and-braces: `dsh-host-webserver.register` performs no authorization
336
+ * check and `ctx.get(name)` only asks whether the providing fiber is active,
337
+ * so the two-channel registration inside this function is what actually
338
+ * covers both arrival orders.
339
+ */
340
+ estimatorCatalogRoute?: boolean
341
+ /**
342
+ * Register the advisory advisor's read-only HTTP report route (decay
343
+ * figure, task summary, score distribution, last benefit-model advice).
344
+ * Same Bundle opt-in semantics as `estimatorCatalogRoute`; the advisor
345
+ * itself stays off until the user turns it on through the
346
+ * `presetOptions.advisor*` settings keys.
347
+ */
348
+ advisorReportRoute?: boolean
349
+ }
350
+
351
+ /** Loader validation for the standalone Bundle opt-in. */
352
+ export const Config: z<Config> = z.object({
353
+ presetOverlay: z.boolean().default(false),
354
+ estimatorCatalogRoute: z.boolean().default(false),
355
+ advisorReportRoute: z.boolean().default(false),
356
+ })
357
+
358
+ /** Register the persisted default read by the currently mounted root pruner. */
359
+ export function apply(ctx: Context, config: Config = {}): void {
360
+ // Measured on the 0.1.2 host: a plugin-load failure surfaces only through the
361
+ // cordis logger, which prints nothing in the `dsh web` terminal — so a throw
362
+ // here is completely invisible and looks exactly like a plugin that loaded
363
+ // and quietly did nothing. Report it to a sink the host shows, then re-throw
364
+ // unchanged: behaviour is untouched, only observability is restored.
365
+ try {
366
+ ctx.inject(['settings'], (settingsCtx) => {
367
+ acquireSettingsRegistration(settingsCtx)
368
+ })
369
+
370
+ // The settings card's host-route dropdowns read this route; it lives on the
371
+ // top-level plugin fiber, not inside the isolated `toolResultPruner`
372
+ // service, because the route is host-wide rather than per-pruner-instance.
373
+ if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
374
+
375
+ // Advisory advisor: read-only decay/score/advice report (opt-in).
376
+ if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx)
377
+
378
+ if (config.presetOverlay !== true) return
379
+
380
+ ctx.inject(['agentPresets'], (presetsCtx) => {
381
+ const installation = decorateAgentPresets(
382
+ presetsCtx.agentPresets,
383
+ {
384
+ modules: resolveCompressionModulePaths(),
385
+ excludedPresetIds: ['minimal'],
386
+ autoCompactThresholdPercent: () => resolveAutoCompactThresholdPercent(presetsCtx),
387
+ },
388
+ )
389
+ presetsCtx.effect(() => () => installation.dispose(), 'contextCompressionSelector.agentPresets()')
390
+ })
391
+ } catch (error) {
392
+ console.error('context-compression selector apply failed: %o', error)
393
+ throw error
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Read the current Auto Compact threshold ratio at composition time. Settings
399
+ * values are revalidated here, and any unreadable value falls back to the 80%
400
+ * default rather than blocking preset composition.
401
+ */
402
+ function resolveAutoCompactThresholdPercent(presetsCtx: Context): number {
403
+ const raw = presetsCtx.get('settings')?.get(CONTEXT_COMPRESSION_NAMESPACE)
404
+ try {
405
+ const record = structuredClone(raw) as Record<string, unknown> | undefined
406
+ const threshold = record?.autoCompact as { thresholdPercent?: number } | undefined
407
+ const value = typeof threshold?.thresholdPercent === 'number' ? threshold.thresholdPercent : 80
408
+ return Number.isFinite(value) && value >= 50 && value <= 90 ? value : 80
409
+ } catch {
410
+ return 80
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Lease one native settings registration across duplicate Host rows.
416
+ *
417
+ * The lease effect is intentionally registered before settings.register().
418
+ * Cordis disposes effects in reverse order, so the native registration first
419
+ * releases the namespace; this disposer can then transfer it to another live
420
+ * owner without a duplicate-registration window.
421
+ */
422
+ function acquireSettingsRegistration(ctx: Context): void {
423
+ const settings = ctx.settings as SettingsCarrier
424
+ const owner: SettingsOwner = { settings }
425
+ let shared = settings[SHARED_SETTINGS]
426
+ if (shared === undefined) {
427
+ shared = {
428
+ owners: new Set(),
429
+ registrationOwner: owner,
430
+ scope: undefined as unknown as SettingsScope<unknown>,
431
+ }
432
+ Object.defineProperty(settings, SHARED_SETTINGS, {
433
+ configurable: true,
434
+ enumerable: false,
435
+ writable: false,
436
+ value: shared,
437
+ })
438
+ }
439
+ shared.owners.add(owner)
440
+ const state = shared
441
+
442
+ ctx.effect(() => () => {
443
+ state.owners.delete(owner)
444
+ if (state.registrationOwner === owner && state.owners.size > 0) {
445
+ const next = state.owners.values().next().value as SettingsOwner
446
+ state.registrationOwner = next
447
+ state.scope = next.settings.register(
448
+ CONTEXT_COMPRESSION_NAMESPACE,
449
+ ContextCompressionSettingsSchema,
450
+ )
451
+ }
452
+ if (state.owners.size === 0 && settings[SHARED_SETTINGS] === state) {
453
+ Reflect.deleteProperty(settings, SHARED_SETTINGS)
454
+ }
455
+ }, 'contextCompressionSelector.settingsLease()')
456
+
457
+ if (state.owners.size === 1) {
458
+ state.scope = settings.register(
459
+ CONTEXT_COMPRESSION_NAMESPACE,
460
+ ContextCompressionSettingsSchema,
461
+ )
462
+ }
463
+ }