dsh-context-compression-improved 0.5.1 → 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.
- package/CHANGELOG.ja.md +36 -0
- package/CHANGELOG.ko.md +36 -0
- package/CHANGELOG.md +38 -0
- package/CHANGELOG.zh.md +32 -0
- package/package.json +1 -1
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +16 -234
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +277 -378
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +196 -234
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +463 -710
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +50 -73
- package/packages/selector/src/pruner.ts +2402 -2730
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +16 -0
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +16 -10
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- 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
|
-
|
|
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
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
+
}
|