dsh-context-compression-improved 0.4.0-beta.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.ja.md +68 -36
- package/CHANGELOG.ko.md +67 -35
- package/CHANGELOG.md +195 -134
- package/CHANGELOG.zh.md +64 -36
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/installation.ja.md +2 -2
- package/docs/installation.ko.md +2 -2
- package/docs/installation.md +103 -78
- package/docs/installation.zh.md +100 -77
- package/docs/repair-log.md +54 -0
- package/package.json +1 -1
- package/packages/selector/lib/{config.js → advisor-state.js} +329 -5
- package/packages/selector/lib/client.d.ts +7 -0
- package/packages/selector/lib/client.js +33 -3
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +112 -3
- package/packages/selector/lib/pruner.d.ts +128 -1
- package/packages/selector/lib/pruner.js +2802 -1374
- package/packages/selector/src/client/ReviewOverlay.tsx +1 -1
- package/packages/selector/src/client/index.ts +1 -1
- package/packages/selector/src/client/preset-options.ts +2 -0
- package/packages/selector/src/index.ts +129 -49
- package/packages/selector/src/profiles.ts +48 -0
- package/packages/selector/src/pruner/content.ts +18 -5
- package/packages/selector/src/pruner/state.ts +3 -0
- package/packages/selector/src/pruner/types.ts +23 -5
- package/packages/selector/src/pruner.ts +297 -162
- package/packages/selector/src/runtime/adaptive-cost.ts +23 -12
- package/packages/selector/src/runtime/audit.ts +40 -2
- package/packages/selector/src/runtime/config.ts +88 -1
- package/packages/selector/src/runtime/measurement.ts +31 -2
- package/packages/selector/src/runtime/reducers.ts +1115 -97
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -0
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +133 -0
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -0
- package/packages/selector/src/runtime/tokenpilot/dedup.ts +1 -1
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +8 -118
- package/packages/selector/src/runtime/tokenpilot/locator.ts +1 -1
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +76 -32
- package/packages/selector/src/runtime/tokenpilot/read-state.ts +23 -2
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +117 -0
- package/packages/selector/src/runtime/tokenpilot/sidechannel.ts +303 -0
- package/packages/selector/src/runtime/toolclass.ts +103 -0
- package/packages/selector/src/runtime/types.ts +37 -0
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
- package/packages/selector/tests/public/package-contract.client.spec.ts +2 -1
- package/packages/selector/tests/review-routes-registry.host.spec.ts +142 -0
- package/packages/selector/tests/runtime/adaptive-cost.spec.ts +7 -7
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -0
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -0
- package/packages/selector/tests/runtime/audit.spec.ts +88 -1
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -0
- package/packages/selector/tests/runtime/code-skeleton.spec.ts +14 -3
- package/packages/selector/tests/runtime/frequency-longstrings.spec.ts +74 -0
- package/packages/selector/tests/runtime/html-reducer.spec.ts +212 -0
- package/packages/selector/tests/runtime/line-mapping.spec.ts +153 -0
- package/packages/selector/tests/runtime/prose-reducers.spec.ts +133 -0
- package/packages/selector/tests/runtime/public/public-runtime.spec.ts +198 -27
- package/packages/selector/tests/runtime/read-input-cap.spec.ts +33 -0
- package/packages/selector/tests/runtime/search-reducer.spec.ts +110 -0
- package/packages/selector/tests/runtime/sidechannel.spec.ts +241 -0
- package/packages/selector/tests/runtime/toc-and-bundled.spec.ts +159 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +194 -0
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +70 -1
- package/packages/selector/tests/runtime/tokenpilot/read-state.spec.ts +24 -0
- package/packages/selector/tests/runtime/toolclass.spec.ts +156 -0
- package/scripts/toolclass-corpus-replay.mjs +281 -0
|
@@ -12,7 +12,7 @@ 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 {
|
|
15
|
+
import { renderReviewOverlay } from './ReviewOverlay.tsx'
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Harness 0.1.5 mounts the web core's `slots` service on the client context
|
|
@@ -25,6 +25,8 @@ const PRESET_OPTION_KEYS = [
|
|
|
25
25
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
26
26
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
27
27
|
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
28
|
+
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
29
|
+
'advisorMinTokens',
|
|
28
30
|
] as const
|
|
29
31
|
|
|
30
32
|
/** One partial edit of `presetOptions`; `undefined` clears the named field. */
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
|
|
12
12
|
ContextCompressionSettingsSchema,
|
|
13
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'
|
|
14
16
|
|
|
15
17
|
// The settings namespace literal and the settings schema are owned by the
|
|
16
18
|
// runtime config module. Both were once inlined/replaced here to dodge a
|
|
@@ -44,66 +46,43 @@ const REVIEW_DECIDE_ROUTES = [
|
|
|
44
46
|
'/api/dsh-context-compression-improved/review-decide',
|
|
45
47
|
] as const
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly benefit: {
|
|
60
|
-
readonly recoveredTokens: number
|
|
61
|
-
readonly penaltyTokens: number
|
|
62
|
-
readonly paybackTurns?: number
|
|
63
|
-
readonly expectedSaving?: number
|
|
64
|
-
}
|
|
65
|
-
readonly enqueuedTurn: number
|
|
66
|
-
readonly lastTurnIndex: number
|
|
67
|
-
}[]
|
|
68
|
-
decideReviewProposal(
|
|
69
|
-
session: unknown,
|
|
70
|
-
proposalId: string,
|
|
71
|
-
decision: 'approved' | 'rejected' | 'ignored',
|
|
72
|
-
): { ok: true } | { ok: false, reason: string } | undefined
|
|
73
|
-
/** Aggregate pending read; absent on older builds (routes then degrade to 503). */
|
|
74
|
-
listAllReviewProposals?(): readonly {
|
|
75
|
-
readonly sessionId: string
|
|
76
|
-
readonly proposals: readonly {
|
|
77
|
-
readonly id: string
|
|
78
|
-
readonly kind: string
|
|
79
|
-
readonly items: readonly { readonly seq: number, readonly kind: string, readonly component: string, readonly tokensBefore: number, readonly tokensAfter: number }[]
|
|
80
|
-
readonly benefit: { readonly recoveredTokens: number, readonly penaltyTokens: number, readonly paybackTurns?: number, readonly expectedSaving?: number }
|
|
81
|
-
readonly enqueuedTurn: number
|
|
82
|
-
readonly lastTurnIndex: number
|
|
83
|
-
}[]
|
|
84
|
-
}[]
|
|
85
|
-
reviewSummary?(session: unknown): {
|
|
86
|
-
readonly autoApplied: number
|
|
87
|
-
readonly reviewApplied: number
|
|
88
|
-
readonly expired: number
|
|
89
|
-
readonly voided: number
|
|
90
|
-
}
|
|
91
|
-
}
|
|
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
|
|
92
61
|
|
|
93
62
|
/** Minimal face of the agents service: session id → agent (carrying the session). */
|
|
94
63
|
interface AgentsServiceLike {
|
|
95
64
|
get?(id: unknown): { session?: unknown } | undefined
|
|
96
65
|
}
|
|
97
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
|
+
*/
|
|
98
76
|
function reviewPrunerOf(readService: (name: string) => unknown): ReviewPrunerLike | undefined {
|
|
99
77
|
const candidate = readService('toolResultPruner') as {
|
|
100
78
|
listReviewProposals?: unknown
|
|
101
79
|
decideReviewProposal?: unknown
|
|
102
80
|
} | undefined
|
|
103
|
-
|
|
104
|
-
&& typeof candidate?.decideReviewProposal === 'function'
|
|
105
|
-
|
|
106
|
-
|
|
81
|
+
if (typeof candidate?.listReviewProposals === 'function'
|
|
82
|
+
&& typeof candidate?.decideReviewProposal === 'function') {
|
|
83
|
+
return candidate as unknown as ReviewPrunerLike
|
|
84
|
+
}
|
|
85
|
+
return resolveReviewPruner()
|
|
107
86
|
}
|
|
108
87
|
|
|
109
88
|
function sessionFor(readService: (name: string) => unknown, sessionId: string): unknown {
|
|
@@ -329,6 +308,96 @@ function registerReviewQueueRoutes(ctx: Context): void {
|
|
|
329
308
|
log('warn', 'context-compression webServer not active yet — review routes pending: %s', REVIEW_QUEUE_ROUTES.join(', '))
|
|
330
309
|
}
|
|
331
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
|
+
|
|
332
401
|
/**
|
|
333
402
|
* The one service the catalog route actually needs. `llm` and
|
|
334
403
|
* `agentDefaultModel` are payload enrichment the handler resolves per request,
|
|
@@ -512,6 +581,13 @@ export interface Config {
|
|
|
512
581
|
* transport and simply never appears.
|
|
513
582
|
*/
|
|
514
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
|
|
515
591
|
}
|
|
516
592
|
|
|
517
593
|
/** Loader validation for the standalone Bundle opt-in. */
|
|
@@ -519,6 +595,7 @@ export const Config: z<Config> = z.object({
|
|
|
519
595
|
presetOverlay: z.boolean().default(false),
|
|
520
596
|
estimatorCatalogRoute: z.boolean().default(false),
|
|
521
597
|
reviewQueueRoute: z.boolean().default(false),
|
|
598
|
+
advisorReportRoute: z.boolean().default(false),
|
|
522
599
|
})
|
|
523
600
|
|
|
524
601
|
/** Register the persisted default read by the currently mounted root pruner. */
|
|
@@ -542,6 +619,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
542
619
|
// config JSDoc for the opt-in semantics).
|
|
543
620
|
if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx)
|
|
544
621
|
|
|
622
|
+
// Advisory advisor: read-only decay/score report (opt-in, like review).
|
|
623
|
+
if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx)
|
|
624
|
+
|
|
545
625
|
if (config.presetOverlay !== true) return
|
|
546
626
|
|
|
547
627
|
ctx.inject(['agentPresets'], (presetsCtx) => {
|
|
@@ -140,6 +140,13 @@ export interface PresetOptionsSettings {
|
|
|
140
140
|
readonly reviewTimeoutTurns?: number
|
|
141
141
|
readonly cacheHitDiscountAlpha?: number
|
|
142
142
|
readonly reviewHighImpactTokens?: number
|
|
143
|
+
/** Advisory advisor channel; `''` (the default) keeps the advisor off. */
|
|
144
|
+
readonly advisorMode?: '' | 'host' | 'direct'
|
|
145
|
+
readonly advisorTimeoutMs?: number
|
|
146
|
+
readonly advisorRefreshTurns?: number
|
|
147
|
+
readonly advisorScoreThreshold?: number
|
|
148
|
+
readonly advisorSampleLimit?: number
|
|
149
|
+
readonly advisorMinTokens?: number
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/**
|
|
@@ -154,6 +161,8 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
154
161
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
155
162
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
156
163
|
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
164
|
+
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
165
|
+
'advisorMinTokens',
|
|
157
166
|
])
|
|
158
167
|
if (Object.keys(value).some(key => !allowed.has(key))) return undefined
|
|
159
168
|
for (const key of ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'reviewMode'] as const) {
|
|
@@ -164,6 +173,10 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
164
173
|
if (estimatorMode !== undefined && estimatorMode !== '' && estimatorMode !== 'host' && estimatorMode !== 'direct') {
|
|
165
174
|
return undefined
|
|
166
175
|
}
|
|
176
|
+
const advisorMode = value.advisorMode
|
|
177
|
+
if (advisorMode !== undefined && advisorMode !== '' && advisorMode !== 'host' && advisorMode !== 'direct') {
|
|
178
|
+
return undefined
|
|
179
|
+
}
|
|
167
180
|
for (const key of ['estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey'] as const) {
|
|
168
181
|
const entry = value[key]
|
|
169
182
|
if (entry !== undefined && typeof entry !== 'string') return undefined
|
|
@@ -191,6 +204,35 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
191
204
|
|| reviewHighImpactTokens < 0)) {
|
|
192
205
|
return undefined
|
|
193
206
|
}
|
|
207
|
+
const advisorTimeoutMs = value.advisorTimeoutMs
|
|
208
|
+
if (advisorTimeoutMs !== undefined
|
|
209
|
+
&& (typeof advisorTimeoutMs !== 'number' || !Number.isSafeInteger(advisorTimeoutMs)
|
|
210
|
+
|| advisorTimeoutMs < 100 || advisorTimeoutMs > 60_000)) {
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
const advisorRefreshTurns = value.advisorRefreshTurns
|
|
214
|
+
if (advisorRefreshTurns !== undefined
|
|
215
|
+
&& (typeof advisorRefreshTurns !== 'number' || !Number.isSafeInteger(advisorRefreshTurns)
|
|
216
|
+
|| advisorRefreshTurns < 1)) {
|
|
217
|
+
return undefined
|
|
218
|
+
}
|
|
219
|
+
const advisorScoreThreshold = value.advisorScoreThreshold
|
|
220
|
+
if (advisorScoreThreshold !== undefined
|
|
221
|
+
&& (typeof advisorScoreThreshold !== 'number' || !Number.isFinite(advisorScoreThreshold)
|
|
222
|
+
|| advisorScoreThreshold <= 0 || advisorScoreThreshold >= 1)) {
|
|
223
|
+
return undefined
|
|
224
|
+
}
|
|
225
|
+
const advisorSampleLimit = value.advisorSampleLimit
|
|
226
|
+
if (advisorSampleLimit !== undefined
|
|
227
|
+
&& (typeof advisorSampleLimit !== 'number' || !Number.isSafeInteger(advisorSampleLimit)
|
|
228
|
+
|| advisorSampleLimit < 1 || advisorSampleLimit > 64)) {
|
|
229
|
+
return undefined
|
|
230
|
+
}
|
|
231
|
+
const advisorMinTokens = value.advisorMinTokens
|
|
232
|
+
if (advisorMinTokens !== undefined
|
|
233
|
+
&& (typeof advisorMinTokens !== 'number' || !Number.isSafeInteger(advisorMinTokens) || advisorMinTokens < 1)) {
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
194
236
|
const decoded: {
|
|
195
237
|
-readonly [K in keyof PresetOptionsSettings]: PresetOptionsSettings[K]
|
|
196
238
|
} = {}
|
|
@@ -208,6 +250,12 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
208
250
|
if (reviewTimeoutTurns !== undefined) decoded.reviewTimeoutTurns = reviewTimeoutTurns as number
|
|
209
251
|
if (cacheHitDiscountAlpha !== undefined) decoded.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
|
|
210
252
|
if (reviewHighImpactTokens !== undefined) decoded.reviewHighImpactTokens = reviewHighImpactTokens as number
|
|
253
|
+
if (advisorMode !== undefined) decoded.advisorMode = advisorMode as '' | 'host' | 'direct'
|
|
254
|
+
if (advisorTimeoutMs !== undefined) decoded.advisorTimeoutMs = advisorTimeoutMs as number
|
|
255
|
+
if (advisorRefreshTurns !== undefined) decoded.advisorRefreshTurns = advisorRefreshTurns as number
|
|
256
|
+
if (advisorScoreThreshold !== undefined) decoded.advisorScoreThreshold = advisorScoreThreshold as number
|
|
257
|
+
if (advisorSampleLimit !== undefined) decoded.advisorSampleLimit = advisorSampleLimit as number
|
|
258
|
+
if (advisorMinTokens !== undefined) decoded.advisorMinTokens = advisorMinTokens as number
|
|
211
259
|
return decoded
|
|
212
260
|
}
|
|
213
261
|
|
|
@@ -70,8 +70,11 @@ function unavailableCount(reason: string): TokenCount {
|
|
|
70
70
|
|
|
71
71
|
// ── Recovery markers ────────────────────────────────────────────────────
|
|
72
72
|
|
|
73
|
-
function recoveryMarker(sourceRef: string, label: string): string {
|
|
74
|
-
|
|
73
|
+
function recoveryMarker(sourceRef: string, label: string, startLine?: number): string {
|
|
74
|
+
const hint = startLine === undefined
|
|
75
|
+
? 'use context_compression_retrieve if needed'
|
|
76
|
+
: `retrieve with context_compression_retrieve({"ref":"${sourceRef}","start_line":${String(startLine)},"max_lines":80})`
|
|
77
|
+
return `\n\n[... ${label}; source=${sourceRef}; ${hint} ...]\n\n`
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
// ── Content measurement ─────────────────────────────────────────────────
|
|
@@ -124,11 +127,11 @@ function nativePruneContent(
|
|
|
124
127
|
thresholdChars: number,
|
|
125
128
|
headChars: number,
|
|
126
129
|
tailChars: number,
|
|
127
|
-
marker: string = PRUNE_MARKER,
|
|
130
|
+
marker: string | ((startLine: number) => string) = PRUNE_MARKER,
|
|
128
131
|
): ContentBlock[] | null {
|
|
129
132
|
const totalChars = measureContent(blocks)
|
|
130
133
|
if (totalChars <= thresholdChars) return null
|
|
131
|
-
const markerChars = codePointLength(marker)
|
|
134
|
+
const markerChars = codePointLength(typeof marker === 'function' ? marker(1) : marker)
|
|
132
135
|
const safeHead = Math.max(0, Math.min(headChars, thresholdChars - markerChars))
|
|
133
136
|
const safeTail = Math.max(0, Math.min(tailChars, thresholdChars - markerChars - safeHead))
|
|
134
137
|
const removedStart = safeHead
|
|
@@ -136,9 +139,14 @@ function nativePruneContent(
|
|
|
136
139
|
const pruned: ContentBlock[] = []
|
|
137
140
|
let consumed = 0
|
|
138
141
|
let markerInserted = false
|
|
142
|
+
// '\n' count in the RAW event text before the current block (retrieve scans
|
|
143
|
+
// the original event; blocks are joined with '\n'). Line numbers therefore
|
|
144
|
+
// never depend on what the pruning removed.
|
|
145
|
+
let newlinesBefore = 0
|
|
139
146
|
for (const block of blocks) {
|
|
140
147
|
if (block.type !== 'text') {
|
|
141
148
|
pruned.push(block)
|
|
149
|
+
newlinesBefore += 1
|
|
142
150
|
continue
|
|
143
151
|
}
|
|
144
152
|
const points = Array.from(block.text)
|
|
@@ -147,10 +155,15 @@ function nativePruneContent(
|
|
|
147
155
|
const headEnd = Math.min(points.length, Math.max(0, removedStart - blockStart))
|
|
148
156
|
const tailStart = Math.min(points.length, Math.max(0, removedEnd - blockStart))
|
|
149
157
|
const intersectsRemoved = blockStart < removedEnd && blockEnd > removedStart
|
|
150
|
-
|
|
158
|
+
// The elided region starts on the event line after the retained head.
|
|
159
|
+
const headText = points.slice(0, headEnd).join('')
|
|
160
|
+
const insertion = intersectsRemoved && !markerInserted && typeof marker === 'function'
|
|
161
|
+
? marker(1 + newlinesBefore + headText.split('\n').length - 1)
|
|
162
|
+
: intersectsRemoved && !markerInserted ? marker : ''
|
|
151
163
|
if (insertion !== '') markerInserted = true
|
|
152
164
|
const text = points.slice(0, headEnd).join('') + insertion + points.slice(tailStart).join('')
|
|
153
165
|
if (text !== '') pruned.push({ ...block, text })
|
|
166
|
+
newlinesBefore += block.text.split('\n').length - 1 + 1
|
|
154
167
|
consumed = blockEnd
|
|
155
168
|
}
|
|
156
169
|
if (!markerInserted) return null
|
|
@@ -9,6 +9,7 @@ import type { Session } from '@deepseek-ai/dsh-session'
|
|
|
9
9
|
import type { DedupeTable } from '../runtime/tokenpilot/dedup.ts'
|
|
10
10
|
import type { EstimatorFailures } from '../runtime/tokenpilot/estimator.ts'
|
|
11
11
|
import type { ReviewQueue, ReviewQueueStore } from '../runtime/tokenpilot/review-queue.ts'
|
|
12
|
+
import type { SideChannel } from '../runtime/tokenpilot/sidechannel.ts'
|
|
12
13
|
import type {
|
|
13
14
|
ContextCompressionSettings,
|
|
14
15
|
ResolvedConfig,
|
|
@@ -65,6 +66,8 @@ export interface PrunerState {
|
|
|
65
66
|
readonly reviewClocks: WeakMap<Session, number>
|
|
66
67
|
/** Estimator-reported remaining turns Ŝ per Session; advisory only. */
|
|
67
68
|
readonly estimatorRemainingTurns: WeakMap<Session, number>
|
|
69
|
+
/** Per-session advisor side channel, constructed once with the advisor overrides. */
|
|
70
|
+
readonly advisorChannels: WeakMap<Session, SideChannel>
|
|
68
71
|
/** Four-state outcome counters per Session (floating-window summary row). */
|
|
69
72
|
readonly reviewSummaries: WeakMap<Session, ReviewSessionSummary>
|
|
70
73
|
}
|
|
@@ -20,11 +20,16 @@ export interface SnapshotCandidate {
|
|
|
20
20
|
readonly seq: number
|
|
21
21
|
readonly event: SessionEvent<'tool/result'>
|
|
22
22
|
readonly call: ToolCallInfo
|
|
23
|
-
/** Authoritative same-revision exact canonical content count. */
|
|
23
|
+
/** Authoritative same-revision exact canonical content count (telemetry only). */
|
|
24
24
|
readonly count: TokenCount
|
|
25
25
|
/** Same-revision legacy heuristic price used only by bounded projections. */
|
|
26
26
|
readonly shadowedHeuristicTokenCount: number
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Authoritative decision metric: character pressure in Unicode code points.
|
|
29
|
+
* Every eligibility and acceptance gate decides on this number so the plugin
|
|
30
|
+
* never depends on the routed model id owning a bundled exact tokenizer;
|
|
31
|
+
* `count` is retained as diagnostic telemetry only.
|
|
32
|
+
*/
|
|
28
33
|
readonly characterPressure: number
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -40,19 +45,32 @@ export interface PlannedReplacement {
|
|
|
40
45
|
readonly charsAfter: number
|
|
41
46
|
readonly tokensBefore: number
|
|
42
47
|
readonly tokensAfter: number
|
|
48
|
+
/**
|
|
49
|
+
* Which measurement produced this plan's decision: 'exact-tokenizer' when
|
|
50
|
+
* both sides of the reduction carry one bundled tokenizer identity,
|
|
51
|
+
* 'characters' when the proof ran on code points alone.
|
|
52
|
+
*/
|
|
53
|
+
readonly measurementBasis: 'exact-tokenizer' | 'characters'
|
|
43
54
|
readonly tokenizerId: string
|
|
44
55
|
readonly tokenizerRevision: string
|
|
56
|
+
/**
|
|
57
|
+
* Original-event lines the reducer elided (task_4c/G7 telemetry, from
|
|
58
|
+
* `ReducerOutput.elidedLines`). Rides the rewrite AUDIT record only — it is
|
|
59
|
+
* never printed into replacement content.
|
|
60
|
+
*/
|
|
61
|
+
readonly elidedLines?: number
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
/**
|
|
48
65
|
* Discriminated History planning outcome: every skip path carries its own
|
|
49
66
|
* reason instead of collapsing into one merged "no eligible minimum reclaim"
|
|
50
|
-
* audit, so operators can tell a protected working set from
|
|
51
|
-
*
|
|
67
|
+
* audit, so operators can tell a protected working set from an unreachable
|
|
68
|
+
* reclaim target. The former 'exact-tokenizer-unavailable' member is gone:
|
|
69
|
+
* planning decides on the character basis and can no longer refuse for a
|
|
70
|
+
* missing exact count.
|
|
52
71
|
*/
|
|
53
72
|
export type HistoryPlanOutcome =
|
|
54
73
|
| { readonly kind: 'planned', readonly plans: PlannedReplacement[] }
|
|
55
|
-
| { readonly kind: 'exact-tokenizer-unavailable' }
|
|
56
74
|
| { readonly kind: 'below-profile-trigger' }
|
|
57
75
|
| { readonly kind: 'no-safe-candidates' }
|
|
58
76
|
| { readonly kind: 'protected-working-set' }
|