dsh-context-compression-improved 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/CHANGELOG.ja.md +144 -83
- package/CHANGELOG.ko.md +143 -82
- package/CHANGELOG.md +278 -212
- package/CHANGELOG.zh.md +131 -77
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- 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 +0 -101
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +0 -38
- 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 +24 -271
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +2 -25
- package/packages/selector/src/pruner.ts +75 -403
- 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-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- 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 +45 -14
- package/scripts/toolclass-corpus-replay.mjs +281 -281
- 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
|
@@ -275,104 +275,3 @@ export function EstimatorControls({ options, disabled, save, settle, t }: Estima
|
|
|
275
275
|
</section>
|
|
276
276
|
)
|
|
277
277
|
}
|
|
278
|
-
|
|
279
|
-
interface ReviewModeControlsProps {
|
|
280
|
-
options: PresetOptionsSettings
|
|
281
|
-
disabled: boolean
|
|
282
|
-
save: (options: PresetOptionsPatch) => Promise<void>
|
|
283
|
-
settle: (operation: () => Promise<void>) => void
|
|
284
|
-
t: (key: ContextCompressionLocaleKey) => string
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* TokenPilot-inspired review-mode card (beta). When enabled, edge/high-impact
|
|
289
|
-
* reduction candidates queue for manual approval and execute in one merged
|
|
290
|
-
* batch at the next turn boundary; the numeric fields tune the benefit model.
|
|
291
|
-
* Numeric drafts commit on blur and only when they parse to a value the
|
|
292
|
-
* runtime schema accepts, so an invalid keystroke never disables the panel.
|
|
293
|
-
*/
|
|
294
|
-
export function ReviewModeControls({ options, disabled, save, settle, t }: ReviewModeControlsProps) {
|
|
295
|
-
const [turnsDraft, setTurnsDraft] = useState(String(options.reviewTimeoutTurns ?? 6))
|
|
296
|
-
const [alphaDraft, setAlphaDraft] = useState(String(options.cacheHitDiscountAlpha ?? 0.1))
|
|
297
|
-
const [highImpactDraft, setHighImpactDraft] = useState(String(options.reviewHighImpactTokens ?? 4000))
|
|
298
|
-
const reviewMode = options.reviewMode ?? false
|
|
299
|
-
const commit = (patch: PresetOptionsPatch) => {
|
|
300
|
-
settle(() => save(patch))
|
|
301
|
-
}
|
|
302
|
-
const commitTurns = (): void => {
|
|
303
|
-
const next = Number(turnsDraft)
|
|
304
|
-
if (!Number.isSafeInteger(next) || next < 1 || next === (options.reviewTimeoutTurns ?? 6)) return
|
|
305
|
-
commit({ reviewTimeoutTurns: next })
|
|
306
|
-
}
|
|
307
|
-
const commitAlpha = (): void => {
|
|
308
|
-
const next = Number(alphaDraft)
|
|
309
|
-
if (!Number.isFinite(next) || next <= 0 || next >= 1 || next === (options.cacheHitDiscountAlpha ?? 0.1)) return
|
|
310
|
-
commit({ cacheHitDiscountAlpha: next })
|
|
311
|
-
}
|
|
312
|
-
const commitHighImpact = (): void => {
|
|
313
|
-
const next = Number(highImpactDraft)
|
|
314
|
-
if (!Number.isSafeInteger(next) || next < 0 || next === (options.reviewHighImpactTokens ?? 4000)) return
|
|
315
|
-
commit({ reviewHighImpactTokens: next })
|
|
316
|
-
}
|
|
317
|
-
return (
|
|
318
|
-
<section className={css.autoCompact} aria-labelledby="context-compression-review-title">
|
|
319
|
-
<h3 id="context-compression-review-title" className={css.autoCompactTitle}>{t('review.title')}</h3>
|
|
320
|
-
<p className={css.customNote}>{t('review.description')}</p>
|
|
321
|
-
<label className={css.field}>
|
|
322
|
-
<span>{t('review.enabled')}</span>
|
|
323
|
-
<select
|
|
324
|
-
value={reviewMode ? 'on' : 'off'}
|
|
325
|
-
disabled={disabled}
|
|
326
|
-
onChange={(event) => { settle(() => save({ reviewMode: event.currentTarget.value === 'on' })) }}
|
|
327
|
-
>
|
|
328
|
-
<option value="off">{t('review.enabled.off')}</option>
|
|
329
|
-
<option value="on">{t('review.enabled.on')}</option>
|
|
330
|
-
</select>
|
|
331
|
-
</label>
|
|
332
|
-
{reviewMode ? (
|
|
333
|
-
<>
|
|
334
|
-
<label className={css.field}>
|
|
335
|
-
<span>{t('review.timeoutTurns')}</span>
|
|
336
|
-
<input
|
|
337
|
-
type="number"
|
|
338
|
-
min={1}
|
|
339
|
-
step={1}
|
|
340
|
-
value={turnsDraft}
|
|
341
|
-
disabled={disabled}
|
|
342
|
-
onChange={(event) => { setTurnsDraft(event.currentTarget.value) }}
|
|
343
|
-
onBlur={commitTurns}
|
|
344
|
-
onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }}
|
|
345
|
-
/>
|
|
346
|
-
</label>
|
|
347
|
-
<label className={css.field}>
|
|
348
|
-
<span>{t('review.alpha')}</span>
|
|
349
|
-
<input
|
|
350
|
-
type="number"
|
|
351
|
-
min={0.01}
|
|
352
|
-
max={0.99}
|
|
353
|
-
step={0.05}
|
|
354
|
-
value={alphaDraft}
|
|
355
|
-
disabled={disabled}
|
|
356
|
-
onChange={(event) => { setAlphaDraft(event.currentTarget.value) }}
|
|
357
|
-
onBlur={commitAlpha}
|
|
358
|
-
onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }}
|
|
359
|
-
/>
|
|
360
|
-
</label>
|
|
361
|
-
<label className={css.field}>
|
|
362
|
-
<span>{t('review.highImpact')}</span>
|
|
363
|
-
<input
|
|
364
|
-
type="number"
|
|
365
|
-
min={0}
|
|
366
|
-
step={500}
|
|
367
|
-
value={highImpactDraft}
|
|
368
|
-
disabled={disabled}
|
|
369
|
-
onChange={(event) => { setHighImpactDraft(event.currentTarget.value) }}
|
|
370
|
-
onBlur={commitHighImpact}
|
|
371
|
-
onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur() }}
|
|
372
|
-
/>
|
|
373
|
-
</label>
|
|
374
|
-
</>
|
|
375
|
-
) : null}
|
|
376
|
-
</section>
|
|
377
|
-
)
|
|
378
|
-
}
|
|
@@ -12,7 +12,6 @@ import { DEFAULT_CUSTOM_COMPRESSION_POLICY } from '../profiles.ts'
|
|
|
12
12
|
import { decodeSettings } from './decode.ts'
|
|
13
13
|
import { en, zh } from './locales.ts'
|
|
14
14
|
import { planPresetOptionsOps, presetOptionsOpsAccepted } from './preset-options.ts'
|
|
15
|
-
import { renderReviewOverlay } from './ReviewOverlay.tsx'
|
|
16
15
|
|
|
17
16
|
/**
|
|
18
17
|
* Harness 0.1.5 mounts the web core's `slots` service on the client context
|
|
@@ -142,22 +141,6 @@ export function apply(ctx: ClientContext): void {
|
|
|
142
141
|
} catch (error) {
|
|
143
142
|
console.warn('[dsh-context-compression-improved] settings.section 注册失败(新宿主已收编):', error)
|
|
144
143
|
}
|
|
145
|
-
|
|
146
|
-
// TokenPilot-inspired R4:审查浮窗挂在 shell.overlay(dsh-tidychat 先例:
|
|
147
|
-
// 该层默认点击穿透,卡片自持指针事件)。reviewMode 关闭或无 pending 时组件
|
|
148
|
-
// 渲染 null —— 与 0.1.2 宿主(无此 slot)同构的降级语义:注册失败不影响设置卡。
|
|
149
|
-
// (本文件是 .ts:元素构造在 ReviewOverlay.renderReviewOverlay,不能内联 JSX。)
|
|
150
|
-
try {
|
|
151
|
-
ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
152
|
-
{ name: 'shell.overlay', id: 'context-compression-review' },
|
|
153
|
-
() => {
|
|
154
|
-
const scope = ctx.settingsScope.bind<ContextCompressionSettings>({ namespace: NS, decode: decodeSettings })
|
|
155
|
-
return renderReviewOverlay(scope, ctx.locale.bind(NS) as (key: string) => string)
|
|
156
|
-
},
|
|
157
|
-
))
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.warn('[dsh-context-compression-improved] shell.overlay 注册失败(宿主无浮层或已收编):', error)
|
|
160
|
-
}
|
|
161
144
|
}
|
|
162
145
|
|
|
163
146
|
export type {
|
|
@@ -35,25 +35,6 @@ export const zh = {
|
|
|
35
35
|
'estimator.apiKey.set': '已设置 · 输入新值覆盖',
|
|
36
36
|
'estimator.apiKey.clear': '清除',
|
|
37
37
|
'estimator.apiKey.overwrite': '已设置保密值,输入新值并失焦即可覆盖。',
|
|
38
|
-
'review.title': '人工审查(beta)',
|
|
39
|
-
'review.description': '开启后,边缘区间与高影响的压缩候选不再自动执行,而是进入待审队列并在你批准后的下一个回合边界批量执行;未处理的提案超过过期轮数后自动作废。仅随「TokenPilot 启发模式」提供,默认关闭。',
|
|
40
|
-
'review.enabled': '审查模式',
|
|
41
|
-
'review.enabled.on': '开(提案等待人工批准)',
|
|
42
|
-
'review.enabled.off': '关(默认,全自动)',
|
|
43
|
-
'review.timeoutTurns': '提案过期轮数',
|
|
44
|
-
'review.alpha': '缓存命中折扣 α',
|
|
45
|
-
'review.highImpact': '高影响门槛(tokens)',
|
|
46
|
-
'review.badge': '待审',
|
|
47
|
-
'review.summary.autoApplied': '自动应用',
|
|
48
|
-
'review.summary.reviewApplied': '审查应用',
|
|
49
|
-
'review.summary.expired': '已过期',
|
|
50
|
-
'review.summary.voided': '已作废',
|
|
51
|
-
'review.row.payback': '回本轮数',
|
|
52
|
-
'review.row.expectedSaving': '预期节省',
|
|
53
|
-
'review.row.estimated': '估计',
|
|
54
|
-
'review.action.approve': '批准',
|
|
55
|
-
'review.action.reject': '驳回',
|
|
56
|
-
'review.action.ignore': '本会话忽略',
|
|
57
38
|
'detail.tokenpilot-inspired': '在平衡模式之上叠加去重指针、恢复豁免、摘要定位块、前缀稳定与读取状态语义;估计器需另行配置端点',
|
|
58
39
|
'profile.custom': 'Custom/实验模式',
|
|
59
40
|
'profile.native': '原生对照',
|
|
@@ -154,25 +135,6 @@ export const en = {
|
|
|
154
135
|
'estimator.apiKey.set': 'Set · type a new value to overwrite',
|
|
155
136
|
'estimator.apiKey.clear': 'Clear',
|
|
156
137
|
'estimator.apiKey.overwrite': 'A secret is stored; type a new value and blur to overwrite it.',
|
|
157
|
-
'review.title': 'Review mode (beta)',
|
|
158
|
-
'review.description': 'When enabled, edge-band and high-impact reduction candidates no longer apply automatically: they queue for manual approval and execute in one merged batch at the next turn boundary after approval. Unhandled proposals expire after the configured number of turns. Ships with the TokenPilot-inspired profile only, off by default.',
|
|
159
|
-
'review.enabled': 'Review mode',
|
|
160
|
-
'review.enabled.on': 'On (proposals wait for manual approval)',
|
|
161
|
-
'review.enabled.off': 'Off (default, fully automatic)',
|
|
162
|
-
'review.timeoutTurns': 'Proposal expiry (turns)',
|
|
163
|
-
'review.alpha': 'Cache-hit discount α',
|
|
164
|
-
'review.highImpact': 'High-impact threshold (tokens)',
|
|
165
|
-
'review.badge': 'Review',
|
|
166
|
-
'review.summary.autoApplied': 'Auto-applied',
|
|
167
|
-
'review.summary.reviewApplied': 'Review-applied',
|
|
168
|
-
'review.summary.expired': 'Expired',
|
|
169
|
-
'review.summary.voided': 'Voided',
|
|
170
|
-
'review.row.payback': 'Payback',
|
|
171
|
-
'review.row.expectedSaving': 'Expected saving',
|
|
172
|
-
'review.row.estimated': 'estimated',
|
|
173
|
-
'review.action.approve': 'Approve',
|
|
174
|
-
'review.action.reject': 'Reject',
|
|
175
|
-
'review.action.ignore': 'Ignore',
|
|
176
138
|
'detail.tokenpilot-inspired': 'Layered on Balanced: dedupe pointers, recovery exemption, summary locators, prefix stabilization, and read-state semantics; the estimator needs an endpoint configured separately',
|
|
177
139
|
'profile.custom': 'Custom / Experimental',
|
|
178
140
|
'profile.native': 'Native baseline',
|
|
@@ -20,11 +20,12 @@ import type { PresetOptionsSettings } from '../profiles.ts'
|
|
|
20
20
|
/** The namespace key holding every tokenpilot-inspired sub-capability override. */
|
|
21
21
|
const PRESET_OPTIONS_KEY = 'presetOptions'
|
|
22
22
|
|
|
23
|
-
/** Every field a patch may address, in the schema's own order.
|
|
23
|
+
/** Every field a patch may address, in the schema's own order. The retired
|
|
24
|
+
* review-gate keys are deliberately absent: nothing writes them any more, and
|
|
25
|
+
* a stored document that still carries them is tolerated by the decoders. */
|
|
24
26
|
const PRESET_OPTION_KEYS = [
|
|
25
27
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
26
28
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
27
|
-
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
28
29
|
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
29
30
|
'advisorMinTokens',
|
|
30
31
|
] as const
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '../profiles.ts'
|
|
17
17
|
import type { CompressionProfileSelectorProps } from './CompressionProfileSelector.tsx'
|
|
18
18
|
import { AutoCompactThresholdControls, CodeSkeletonControls } from './CompressionProfileControls.tsx'
|
|
19
|
-
import { EstimatorControls, EstimatorInactiveNotice
|
|
19
|
+
import { EstimatorControls, EstimatorInactiveNotice } from './EstimatorControls.tsx'
|
|
20
20
|
import { CustomPolicyEditor, editableCustom } from './CustomPolicyEditor.tsx'
|
|
21
21
|
|
|
22
22
|
/** Full-page Settings surface backed by the same durable selector state. */
|
|
@@ -105,22 +105,13 @@ export function SettingsCompressionProfileControls({
|
|
|
105
105
|
{current !== 'tokenpilot-inspired' ? (
|
|
106
106
|
<EstimatorInactiveNotice profile={t(`profile.${current}`)} t={t} />
|
|
107
107
|
) : (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
/>
|
|
116
|
-
<ReviewModeControls
|
|
117
|
-
options={state.value?.presetOptions ?? {}}
|
|
118
|
-
disabled={busy || !state.writable || !selectorAvailable}
|
|
119
|
-
save={savePresetOptions}
|
|
120
|
-
settle={settle}
|
|
121
|
-
t={t}
|
|
122
|
-
/>
|
|
123
|
-
</>
|
|
108
|
+
<EstimatorControls
|
|
109
|
+
options={state.value?.presetOptions ?? {}}
|
|
110
|
+
disabled={busy || !state.writable || !selectorAvailable}
|
|
111
|
+
save={savePresetOptions}
|
|
112
|
+
settle={settle}
|
|
113
|
+
t={t}
|
|
114
|
+
/>
|
|
124
115
|
)}
|
|
125
116
|
<div className={css.pricing}>{t('pricing.disclosure')}</div>
|
|
126
117
|
{current !== 'custom' || draft === null || !selectorAvailable ? null : (
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
12
12
|
ContextCompressionSettingsSchema,
|
|
13
13
|
} from './runtime/config.ts'
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
import { getAdvisorState } from './runtime/tokenpilot/advisor-state.ts'
|
|
16
16
|
|
|
17
17
|
// The settings namespace literal and the settings schema are owned by the
|
|
@@ -35,62 +35,23 @@ const ESTIMATOR_CATALOG_ROUTES = [
|
|
|
35
35
|
'/api/dsh-context-compression-improved/estimator-catalog',
|
|
36
36
|
] as const
|
|
37
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
38
|
// Advisory advisor: one read-only report route, dual prefixed like the others.
|
|
50
39
|
const ADVISOR_REPORT_ROUTES = [
|
|
51
40
|
'/endpoint/dsh-context-compression-improved/advisor-report',
|
|
52
41
|
'/api/dsh-context-compression-improved/advisor-report',
|
|
53
42
|
] as const
|
|
54
43
|
|
|
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
44
|
/** Minimal face of the agents service: session id → agent (carrying the session). */
|
|
63
45
|
interface AgentsServiceLike {
|
|
64
46
|
get?(id: unknown): { session?: unknown } | undefined
|
|
65
47
|
}
|
|
66
48
|
|
|
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
49
|
function sessionFor(readService: (name: string) => unknown, sessionId: string): unknown {
|
|
89
50
|
const agents = readService('agents') as AgentsServiceLike | undefined
|
|
90
51
|
return typeof agents?.get === 'function' ? agents.get(sessionId)?.session : undefined
|
|
91
52
|
}
|
|
92
53
|
|
|
93
|
-
function
|
|
54
|
+
function jsonResponse(res: unknown, status: number, body: unknown): void {
|
|
94
55
|
const resTyped = res as {
|
|
95
56
|
writeHead: (code: number, headers?: Record<string, string>) => void
|
|
96
57
|
end: (body?: string) => void
|
|
@@ -99,218 +60,9 @@ function reviewJson(res: unknown, status: number, body: unknown): void {
|
|
|
99
60
|
resTyped.end(JSON.stringify(body))
|
|
100
61
|
}
|
|
101
62
|
|
|
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
63
|
/**
|
|
312
64
|
* Serve the advisory advisor's read-only report route (same registration
|
|
313
|
-
* skeleton as the
|
|
65
|
+
* skeleton as the estimator-catalog route):
|
|
314
66
|
*
|
|
315
67
|
* `GET .../advisor-report?sessionId=…` → the session's prefix-decay figure,
|
|
316
68
|
* the todolist-bound task summary, and the score distribution. Content-free
|
|
@@ -334,7 +86,7 @@ function registerAdvisorReportRoute(ctx: Context): void {
|
|
|
334
86
|
const getHandler = (req: unknown, res: unknown): void => {
|
|
335
87
|
const agents = readService('agents') as AgentsServiceLike | undefined
|
|
336
88
|
if (typeof agents?.get !== 'function') {
|
|
337
|
-
|
|
89
|
+
jsonResponse(res, 503, { ok: false, error: 'advisor report unavailable' })
|
|
338
90
|
return
|
|
339
91
|
}
|
|
340
92
|
let sessionId = ''
|
|
@@ -345,16 +97,16 @@ function registerAdvisorReportRoute(ctx: Context): void {
|
|
|
345
97
|
// Malformed URL: fall through with the empty sessionId already set.
|
|
346
98
|
}
|
|
347
99
|
if (sessionId === '') {
|
|
348
|
-
|
|
100
|
+
jsonResponse(res, 400, { ok: false, error: 'sessionId is required' })
|
|
349
101
|
return
|
|
350
102
|
}
|
|
351
103
|
const session = sessionFor(readService, sessionId)
|
|
352
104
|
if (session === undefined) {
|
|
353
|
-
|
|
105
|
+
jsonResponse(res, 404, { ok: false, error: 'unknown session' })
|
|
354
106
|
return
|
|
355
107
|
}
|
|
356
108
|
const state = getAdvisorState(session as Parameters<typeof getAdvisorState>[0])
|
|
357
|
-
|
|
109
|
+
jsonResponse(res, 200, {
|
|
358
110
|
ok: true,
|
|
359
111
|
sessionId,
|
|
360
112
|
advisor: {
|
|
@@ -364,6 +116,18 @@ function registerAdvisorReportRoute(ctx: Context): void {
|
|
|
364
116
|
decayTurn: state.lastDecay?.turn ?? null,
|
|
365
117
|
scores: [...state.scores].map(([seq, entry]) => ({ seq, score: entry.score, turn: entry.turn })),
|
|
366
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
|
+
},
|
|
367
131
|
},
|
|
368
132
|
})
|
|
369
133
|
}
|
|
@@ -574,18 +338,12 @@ export interface Config {
|
|
|
574
338
|
* covers both arrival orders.
|
|
575
339
|
*/
|
|
576
340
|
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
341
|
/**
|
|
585
342
|
* Register the advisory advisor's read-only HTTP report route (decay
|
|
586
|
-
* figure, task summary, score distribution
|
|
587
|
-
* as `
|
|
588
|
-
* it on through the
|
|
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.
|
|
589
347
|
*/
|
|
590
348
|
advisorReportRoute?: boolean
|
|
591
349
|
}
|
|
@@ -594,7 +352,6 @@ export interface Config {
|
|
|
594
352
|
export const Config: z<Config> = z.object({
|
|
595
353
|
presetOverlay: z.boolean().default(false),
|
|
596
354
|
estimatorCatalogRoute: z.boolean().default(false),
|
|
597
|
-
reviewQueueRoute: z.boolean().default(false),
|
|
598
355
|
advisorReportRoute: z.boolean().default(false),
|
|
599
356
|
})
|
|
600
357
|
|
|
@@ -615,11 +372,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
615
372
|
// service, because the route is host-wide rather than per-pruner-instance.
|
|
616
373
|
if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
|
|
617
374
|
|
|
618
|
-
//
|
|
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).
|
|
375
|
+
// Advisory advisor: read-only decay/score/advice report (opt-in).
|
|
623
376
|
if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx)
|
|
624
377
|
|
|
625
378
|
if (config.presetOverlay !== true) return
|