dsh-context-compression-improved 0.4.0 → 0.5.1
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 +34 -0
- package/CHANGELOG.ko.md +34 -0
- package/CHANGELOG.md +38 -0
- package/CHANGELOG.zh.md +30 -0
- package/docs/installation.md +25 -1
- package/docs/installation.zh.md +24 -1
- package/package.json +1 -1
- package/packages/selector/lib/{review-registry.js → advisor-state.js} +105 -4
- package/packages/selector/lib/client.d.ts +7 -0
- package/packages/selector/lib/client.js +32 -2
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +150 -3
- package/packages/selector/lib/pruner.d.ts +82 -0
- package/packages/selector/lib/pruner.js +545 -11
- package/packages/selector/src/client/preset-options.ts +2 -0
- package/packages/selector/src/index.ts +108 -0
- package/packages/selector/src/preset-overlay.ts +60 -1
- package/packages/selector/src/profiles.ts +48 -0
- package/packages/selector/src/pruner/state.ts +3 -0
- package/packages/selector/src/pruner.ts +113 -0
- package/packages/selector/src/runtime/audit.ts +22 -0
- package/packages/selector/src/runtime/config.ts +58 -0
- 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/sidechannel.ts +24 -9
- package/packages/selector/src/runtime/types.ts +21 -0
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -0
- package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
- 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 +44 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +12 -0
- package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
- package/scripts/packed-components-smoke.mjs +30 -8
- package/scripts/packed-install-e2e.mjs +69 -15
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ContextCompressionSettingsSchema,
|
|
13
13
|
} from './runtime/config.ts'
|
|
14
14
|
import { resolveReviewPruner, type ReviewPrunerFace } from './runtime/tokenpilot/review-registry.ts'
|
|
15
|
+
import { getAdvisorState } from './runtime/tokenpilot/advisor-state.ts'
|
|
15
16
|
|
|
16
17
|
// The settings namespace literal and the settings schema are owned by the
|
|
17
18
|
// runtime config module. Both were once inlined/replaced here to dodge a
|
|
@@ -45,6 +46,12 @@ const REVIEW_DECIDE_ROUTES = [
|
|
|
45
46
|
'/api/dsh-context-compression-improved/review-decide',
|
|
46
47
|
] as const
|
|
47
48
|
|
|
49
|
+
// Advisory advisor: one read-only report route, dual prefixed like the others.
|
|
50
|
+
const ADVISOR_REPORT_ROUTES = [
|
|
51
|
+
'/endpoint/dsh-context-compression-improved/advisor-report',
|
|
52
|
+
'/api/dsh-context-compression-improved/advisor-report',
|
|
53
|
+
] as const
|
|
54
|
+
|
|
48
55
|
/**
|
|
49
56
|
* The review faces of the pruner service the routes consume. Owned by the
|
|
50
57
|
* review registry, which the routes also fall back to when no top-level
|
|
@@ -301,6 +308,96 @@ function registerReviewQueueRoutes(ctx: Context): void {
|
|
|
301
308
|
log('warn', 'context-compression webServer not active yet — review routes pending: %s', REVIEW_QUEUE_ROUTES.join(', '))
|
|
302
309
|
}
|
|
303
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
|
+
|
|
304
401
|
/**
|
|
305
402
|
* The one service the catalog route actually needs. `llm` and
|
|
306
403
|
* `agentDefaultModel` are payload enrichment the handler resolves per request,
|
|
@@ -484,6 +581,13 @@ export interface Config {
|
|
|
484
581
|
* transport and simply never appears.
|
|
485
582
|
*/
|
|
486
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
|
|
487
591
|
}
|
|
488
592
|
|
|
489
593
|
/** Loader validation for the standalone Bundle opt-in. */
|
|
@@ -491,6 +595,7 @@ export const Config: z<Config> = z.object({
|
|
|
491
595
|
presetOverlay: z.boolean().default(false),
|
|
492
596
|
estimatorCatalogRoute: z.boolean().default(false),
|
|
493
597
|
reviewQueueRoute: z.boolean().default(false),
|
|
598
|
+
advisorReportRoute: z.boolean().default(false),
|
|
494
599
|
})
|
|
495
600
|
|
|
496
601
|
/** Register the persisted default read by the currently mounted root pruner. */
|
|
@@ -514,6 +619,9 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
514
619
|
// config JSDoc for the opt-in semantics).
|
|
515
620
|
if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx)
|
|
516
621
|
|
|
622
|
+
// Advisory advisor: read-only decay/score report (opt-in, like review).
|
|
623
|
+
if (config.advisorReportRoute === true) registerAdvisorReportRoute(ctx)
|
|
624
|
+
|
|
517
625
|
if (config.presetOverlay !== true) return
|
|
518
626
|
|
|
519
627
|
ctx.inject(['agentPresets'], (presetsCtx) => {
|
|
@@ -116,6 +116,16 @@ export function standingStampMs(identity: string): number {
|
|
|
116
116
|
return standingStampMsAtWindow(identity, 0)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* True for the filesystem errors a concurrent publish can raise on Windows:
|
|
121
|
+
* `MoveFileEx` reports a lost race — or a reader holding the destination open —
|
|
122
|
+
* as EPERM/EBUSY/EACCES, where POSIX `rename` simply replaces the destination.
|
|
123
|
+
*/
|
|
124
|
+
function isPublishRace(error: unknown): boolean {
|
|
125
|
+
const code = (error as { code?: unknown } | null)?.code
|
|
126
|
+
return code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' || code === 'EEXIST'
|
|
127
|
+
}
|
|
128
|
+
|
|
119
129
|
const COMPRESSION_IDS = new Set([
|
|
120
130
|
'compaction',
|
|
121
131
|
'compaction-basic',
|
|
@@ -224,7 +234,7 @@ class PresetOverlayStore {
|
|
|
224
234
|
// still private. No reader can observe a colliding {mtimeMs,size}
|
|
225
235
|
// between rename and a later corrective utimes call.
|
|
226
236
|
await this.disambiguateStamp(staging, identity)
|
|
227
|
-
await
|
|
237
|
+
await this.publish(staging, path)
|
|
228
238
|
} catch (error) {
|
|
229
239
|
// A failed publish must not leak its staging file into the store; the
|
|
230
240
|
// cleanup must never mask the original failure either.
|
|
@@ -238,6 +248,55 @@ class PresetOverlayStore {
|
|
|
238
248
|
return { ...preset, path }
|
|
239
249
|
}
|
|
240
250
|
|
|
251
|
+
/** In-flight publish chains, one per destination path. */
|
|
252
|
+
private readonly publishChains = new Map<string, Promise<void>>()
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Publish one stamped staging file to its final path, serialized per path.
|
|
256
|
+
*
|
|
257
|
+
* POSIX `rename` replaces an existing destination atomically, so concurrent
|
|
258
|
+
* composers of one identity are naturally idempotent there. Windows
|
|
259
|
+
* `MoveFileEx` instead fails the loser of that race with EPERM/EBUSY, which
|
|
260
|
+
* surfaced as `standingKeyFor()` throwing during concurrent composition.
|
|
261
|
+
* Every composer of one identity writes identical bytes and re-derives the
|
|
262
|
+
* same deterministic identity stamp, so a destination that already observes
|
|
263
|
+
* this staging file's {mtimeMs, size} key IS the intended end state: confirm
|
|
264
|
+
* it and treat the lost race as success. Every other outcome still throws, so
|
|
265
|
+
* a silently reused generation stays forbidden, and the caller still removes
|
|
266
|
+
* the staging file when this rejects.
|
|
267
|
+
*/
|
|
268
|
+
private publish(staging: string, path: string): Promise<void> {
|
|
269
|
+
const previous = this.publishChains.get(path) ?? Promise.resolve()
|
|
270
|
+
const queued = previous.catch(() => undefined).then(async () => {
|
|
271
|
+
try {
|
|
272
|
+
await rename(staging, path)
|
|
273
|
+
return
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (!isPublishRace(error)) throw error
|
|
276
|
+
try {
|
|
277
|
+
const intended = await this.metadataIo.read(staging)
|
|
278
|
+
const published = await this.metadataIo.read(path)
|
|
279
|
+
// Sub-second precision survives a wrapped seconds field, but NTFS
|
|
280
|
+
// rounds it to 100ns, so compare the standing key within that
|
|
281
|
+
// rounding rather than by exact float equality.
|
|
282
|
+
if (published.size === intended.size
|
|
283
|
+
&& Math.abs(published.mtimeMs - intended.mtimeMs) < 2) {
|
|
284
|
+
await rm(staging, { force: true })
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
} catch {
|
|
288
|
+
// The destination is unreadable: report the original publish failure.
|
|
289
|
+
}
|
|
290
|
+
throw error
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
const tracked = queued.finally(() => {
|
|
294
|
+
if (this.publishChains.get(path) === tracked) this.publishChains.delete(path)
|
|
295
|
+
})
|
|
296
|
+
this.publishChains.set(path, tracked)
|
|
297
|
+
return tracked
|
|
298
|
+
}
|
|
299
|
+
|
|
241
300
|
/** Observed {mtimeMs,size} keys published by this store, per identity. */
|
|
242
301
|
private readonly standingKeys = new Map<string, string>()
|
|
243
302
|
|
|
@@ -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
|
|
|
@@ -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
|
}
|
|
@@ -80,6 +80,14 @@ import {
|
|
|
80
80
|
} from './runtime/tokenpilot/review-queue.ts'
|
|
81
81
|
import { registerReviewPruner, sharedReviewStore } from './runtime/tokenpilot/review-registry.ts'
|
|
82
82
|
import { openReviewStorage } from './runtime/tokenpilot/review-storage.ts'
|
|
83
|
+
import { SideChannel } from './runtime/tokenpilot/sidechannel.ts'
|
|
84
|
+
import {
|
|
85
|
+
advisorCandidatePreview,
|
|
86
|
+
collectTailText,
|
|
87
|
+
collectTaskSemantics,
|
|
88
|
+
runSessionAdvisorPass,
|
|
89
|
+
} from './runtime/tokenpilot/advisor.ts'
|
|
90
|
+
import { getAdvisorState } from './runtime/tokenpilot/advisor-state.ts'
|
|
83
91
|
|
|
84
92
|
import {
|
|
85
93
|
DedupeTable,
|
|
@@ -237,6 +245,7 @@ export class ToolResultPruner extends Service {
|
|
|
237
245
|
reviewQueues: new WeakMap(),
|
|
238
246
|
reviewClocks: new WeakMap(),
|
|
239
247
|
estimatorRemainingTurns: new WeakMap(),
|
|
248
|
+
advisorChannels: new WeakMap(),
|
|
240
249
|
reviewSummaries: new WeakMap(),
|
|
241
250
|
}
|
|
242
251
|
|
|
@@ -333,6 +342,10 @@ export class ToolResultPruner extends Service {
|
|
|
333
342
|
// TokenPilot-inspired E1: advisory estimator pass, strictly off the
|
|
334
343
|
// synchronous chain. Verdicts only feed the next pressure pass.
|
|
335
344
|
void this.postflightEstimatorPass(agent.session, signal).catch(() => undefined)
|
|
345
|
+
// Advisory relevance advisor: statistics and suggestions only — its
|
|
346
|
+
// summaries, scores, and decay figure never touch any decision path.
|
|
347
|
+
// Strictly fire-and-forget, with its own backoff state.
|
|
348
|
+
void this.postflightAdvisorPass(agent.session, turn, signal).catch(() => undefined)
|
|
336
349
|
})
|
|
337
350
|
}
|
|
338
351
|
|
|
@@ -644,6 +657,106 @@ export class ToolResultPruner extends Service {
|
|
|
644
657
|
})
|
|
645
658
|
}
|
|
646
659
|
|
|
660
|
+
// ─────────── Advisory relevance advisor (statistics & suggestions only) ───────────
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Advisory advisor pass at the turn boundary, strictly fire-and-forget.
|
|
664
|
+
* Produces todolist-bound tail-task summaries, incremental relevance
|
|
665
|
+
* scores, and a prefix-decay figure — all observational. Every short
|
|
666
|
+
* circuit below (mode off, re-entry, cooldown, no task semantics, no
|
|
667
|
+
* direct endpoint) returns without touching any state the pruning chain
|
|
668
|
+
* reads, so the default configuration adds exactly zero behavior.
|
|
669
|
+
*/
|
|
670
|
+
private async postflightAdvisorPass(session: Session, turn: number, signal: AbortSignal): Promise<void> {
|
|
671
|
+
const policy = this.activePolicy(session)
|
|
672
|
+
const presetOptions = policy?.presetOptions
|
|
673
|
+
const advisor = presetOptions?.advisor
|
|
674
|
+
if (policy === undefined || presetOptions === undefined || advisor === undefined || advisor.mode === '') return
|
|
675
|
+
const advisorState = getAdvisorState(session)
|
|
676
|
+
if (advisorState.inFlight) return
|
|
677
|
+
if (isCoolingDown(advisorState.failures, Date.now())) return
|
|
678
|
+
|
|
679
|
+
const events = sessionEvents(session)
|
|
680
|
+
const task = collectTaskSemantics(events)
|
|
681
|
+
if (task === undefined) return
|
|
682
|
+
|
|
683
|
+
const settings = this.activeSettings(session).presetOptions ?? {}
|
|
684
|
+
if (advisor.mode === 'direct'
|
|
685
|
+
&& (settings.estimatorBaseUrl === undefined || settings.estimatorBaseUrl.length === 0
|
|
686
|
+
|| settings.estimatorModel === undefined || settings.estimatorModel.length === 0)) {
|
|
687
|
+
// The advisor reuses the estimator's direct endpoint; when it is not
|
|
688
|
+
// configured there is nothing to ask, so record the aligned reason and
|
|
689
|
+
// back off instead of re-emitting the audit at every turn.
|
|
690
|
+
emitCompressionAudit(this.ctx.logger, {
|
|
691
|
+
schemaVersion: 1,
|
|
692
|
+
kind: 'advisor-outcome',
|
|
693
|
+
sessionId: String(session.id),
|
|
694
|
+
phase: 'summary',
|
|
695
|
+
channel: 'direct',
|
|
696
|
+
ok: false,
|
|
697
|
+
turnIndex: turn,
|
|
698
|
+
reason: 'no-direct-endpoint',
|
|
699
|
+
latencyMs: 0,
|
|
700
|
+
})
|
|
701
|
+
advisorState.failures = {
|
|
702
|
+
failures: (advisorState.failures?.failures ?? 0) + 1,
|
|
703
|
+
cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1),
|
|
704
|
+
}
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
let channel = this.state.advisorChannels.get(session)
|
|
709
|
+
if (channel === undefined) {
|
|
710
|
+
channel = new SideChannel(this.ctx, settings, {
|
|
711
|
+
mode: advisor.mode,
|
|
712
|
+
timeoutMs: advisor.timeoutMs,
|
|
713
|
+
maxTokens: 512,
|
|
714
|
+
})
|
|
715
|
+
this.state.advisorChannels.set(session, channel)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const view = measureForCompaction(this.ctx, session)
|
|
719
|
+
const candidates = this.snapshot(session, view)
|
|
720
|
+
.filter(candidate => !this.isRecoveryExempt(session, candidate))
|
|
721
|
+
.map(candidate => ({
|
|
722
|
+
seq: candidate.seq,
|
|
723
|
+
characterPressure: candidate.characterPressure,
|
|
724
|
+
preview: advisorCandidatePreview(candidate.call.name, candidate.event.data.message.content),
|
|
725
|
+
}))
|
|
726
|
+
|
|
727
|
+
let sawFailure = false
|
|
728
|
+
const outcome = await runSessionAdvisorPass(session, channel, record => {
|
|
729
|
+
if (record.ok === false) sawFailure = true
|
|
730
|
+
emitCompressionAudit(this.ctx.logger, record)
|
|
731
|
+
}, {
|
|
732
|
+
profile: policy.profile,
|
|
733
|
+
sessionId: String(session.id),
|
|
734
|
+
turn,
|
|
735
|
+
candidates,
|
|
736
|
+
task: {
|
|
737
|
+
source: task.source,
|
|
738
|
+
todoVersion: task.todoVersion,
|
|
739
|
+
taskText: task.taskText,
|
|
740
|
+
},
|
|
741
|
+
advisor: {
|
|
742
|
+
refreshTurns: advisor.refreshTurns,
|
|
743
|
+
scoreThreshold: advisor.scoreThreshold,
|
|
744
|
+
sampleLimit: advisor.sampleLimit,
|
|
745
|
+
minTokens: advisor.minTokens,
|
|
746
|
+
},
|
|
747
|
+
tailText: collectTailText(events),
|
|
748
|
+
signal,
|
|
749
|
+
})
|
|
750
|
+
if (outcome === undefined && sawFailure && signal.aborted === false) {
|
|
751
|
+
advisorState.failures = {
|
|
752
|
+
failures: (advisorState.failures?.failures ?? 0) + 1,
|
|
753
|
+
cooldownUntil: Date.now() + backoffCooldownMs((advisorState.failures?.failures ?? 0) + 1),
|
|
754
|
+
}
|
|
755
|
+
} else if (outcome !== undefined) {
|
|
756
|
+
advisorState.failures = undefined
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
647
760
|
// ─────────── TokenPilot-inspired R4: human-gated review pipeline ───────────
|
|
648
761
|
|
|
649
762
|
/**
|
|
@@ -183,6 +183,27 @@ export interface EstimatorOutcomeAuditRecord extends CompressionAuditBase {
|
|
|
183
183
|
readonly ok: boolean
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/** One background relevance-advisor pass. Only numeric metadata — never prompts, keys, or content. */
|
|
187
|
+
export interface AdvisorOutcomeAuditRecord extends CompressionAuditBase {
|
|
188
|
+
readonly kind: 'advisor-outcome'
|
|
189
|
+
/** Which advisory stage produced this record. */
|
|
190
|
+
readonly phase: 'summary' | 'scoring' | 'decay'
|
|
191
|
+
/** LLM channel; omitted for the locally computed 'decay' phase. */
|
|
192
|
+
readonly channel?: 'host' | 'direct'
|
|
193
|
+
readonly ok: boolean
|
|
194
|
+
/** Whether the pass found candidates to sample (scoring phase). */
|
|
195
|
+
readonly sampledCount?: number
|
|
196
|
+
/** Prefix-decay figure in [0, 1] (decay phase; also emitted by summary/scoring on success). */
|
|
197
|
+
readonly decay?: number
|
|
198
|
+
/** Weighted surface the decay was computed over, in Unicode code points. */
|
|
199
|
+
readonly weightedChars?: number
|
|
200
|
+
/** Turn index the pass ran at. */
|
|
201
|
+
readonly turnIndex?: number
|
|
202
|
+
/** Aligned failure/skip reason code (e.g. 'no-direct-endpoint', 'parse-failed'). */
|
|
203
|
+
readonly reason?: string
|
|
204
|
+
readonly latencyMs: number
|
|
205
|
+
}
|
|
206
|
+
|
|
186
207
|
/** Lifecycle of one human-gated review proposal. Only numeric and enum fields — never content. */
|
|
187
208
|
export interface ReviewOutcomeAuditRecord extends CompressionAuditBase {
|
|
188
209
|
readonly kind: 'review-outcome'
|
|
@@ -219,6 +240,7 @@ export type CompressionAuditRecord =
|
|
|
219
240
|
| NativeAutoCompactAuditRecord
|
|
220
241
|
| SummaryLocatorAuditRecord
|
|
221
242
|
| EstimatorOutcomeAuditRecord
|
|
243
|
+
| AdvisorOutcomeAuditRecord
|
|
222
244
|
| ReviewOutcomeAuditRecord
|
|
223
245
|
|
|
224
246
|
/** Minimal logger method consumed by the audit publisher. */
|
|
@@ -128,6 +128,8 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
128
128
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
129
129
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
130
130
|
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
131
|
+
'advisorMode', 'advisorTimeoutMs', 'advisorRefreshTurns', 'advisorScoreThreshold', 'advisorSampleLimit',
|
|
132
|
+
'advisorMinTokens',
|
|
131
133
|
])
|
|
132
134
|
const unknown = Object.keys(value).find(key => !allowed.has(key))
|
|
133
135
|
if (unknown !== undefined) {
|
|
@@ -144,6 +146,39 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
144
146
|
if (estimatorMode !== undefined && estimatorMode !== '' && estimatorMode !== 'host' && estimatorMode !== 'direct') {
|
|
145
147
|
throw new TypeError('Context-compression presetOptions.estimatorMode must be "", "host", or "direct"')
|
|
146
148
|
}
|
|
149
|
+
const advisorMode = value.advisorMode
|
|
150
|
+
if (advisorMode !== undefined && advisorMode !== '' && advisorMode !== 'host' && advisorMode !== 'direct') {
|
|
151
|
+
throw new TypeError('Context-compression presetOptions.advisorMode must be "", "host", or "direct"')
|
|
152
|
+
}
|
|
153
|
+
const advisorTimeoutMs = value.advisorTimeoutMs
|
|
154
|
+
if (advisorTimeoutMs !== undefined
|
|
155
|
+
&& (typeof advisorTimeoutMs !== 'number' || !Number.isSafeInteger(advisorTimeoutMs)
|
|
156
|
+
|| advisorTimeoutMs < 100 || advisorTimeoutMs > 60_000)) {
|
|
157
|
+
throw new TypeError('Context-compression presetOptions.advisorTimeoutMs must be an integer between 100 and 60000')
|
|
158
|
+
}
|
|
159
|
+
const advisorRefreshTurns = value.advisorRefreshTurns
|
|
160
|
+
if (advisorRefreshTurns !== undefined
|
|
161
|
+
&& (typeof advisorRefreshTurns !== 'number' || !Number.isSafeInteger(advisorRefreshTurns)
|
|
162
|
+
|| advisorRefreshTurns < 1)) {
|
|
163
|
+
throw new TypeError('Context-compression presetOptions.advisorRefreshTurns must be an integer of at least 1')
|
|
164
|
+
}
|
|
165
|
+
const advisorScoreThreshold = value.advisorScoreThreshold
|
|
166
|
+
if (advisorScoreThreshold !== undefined
|
|
167
|
+
&& (typeof advisorScoreThreshold !== 'number' || !Number.isFinite(advisorScoreThreshold)
|
|
168
|
+
|| advisorScoreThreshold <= 0 || advisorScoreThreshold >= 1)) {
|
|
169
|
+
throw new TypeError('Context-compression presetOptions.advisorScoreThreshold must be a number strictly between 0 and 1')
|
|
170
|
+
}
|
|
171
|
+
const advisorSampleLimit = value.advisorSampleLimit
|
|
172
|
+
if (advisorSampleLimit !== undefined
|
|
173
|
+
&& (typeof advisorSampleLimit !== 'number' || !Number.isSafeInteger(advisorSampleLimit)
|
|
174
|
+
|| advisorSampleLimit < 1 || advisorSampleLimit > 64)) {
|
|
175
|
+
throw new TypeError('Context-compression presetOptions.advisorSampleLimit must be an integer between 1 and 64')
|
|
176
|
+
}
|
|
177
|
+
const advisorMinTokens = value.advisorMinTokens
|
|
178
|
+
if (advisorMinTokens !== undefined
|
|
179
|
+
&& (typeof advisorMinTokens !== 'number' || !Number.isSafeInteger(advisorMinTokens) || advisorMinTokens < 1)) {
|
|
180
|
+
throw new TypeError('Context-compression presetOptions.advisorMinTokens must be a positive integer')
|
|
181
|
+
}
|
|
147
182
|
const estimatorTimeoutMs = value.estimatorTimeoutMs
|
|
148
183
|
if (estimatorTimeoutMs !== undefined
|
|
149
184
|
&& (typeof estimatorTimeoutMs !== 'number' || !Number.isSafeInteger(estimatorTimeoutMs)
|
|
@@ -190,6 +225,12 @@ export function parsePresetOptionsSettings(value: unknown): PresetOptionsSetting
|
|
|
190
225
|
if (reviewTimeoutTurns !== undefined) result.reviewTimeoutTurns = reviewTimeoutTurns as number
|
|
191
226
|
if (cacheHitDiscountAlpha !== undefined) result.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
|
|
192
227
|
if (reviewHighImpactTokens !== undefined) result.reviewHighImpactTokens = reviewHighImpactTokens as number
|
|
228
|
+
if (advisorMode !== undefined) result.advisorMode = advisorMode as '' | 'host' | 'direct'
|
|
229
|
+
if (advisorTimeoutMs !== undefined) result.advisorTimeoutMs = advisorTimeoutMs as number
|
|
230
|
+
if (advisorRefreshTurns !== undefined) result.advisorRefreshTurns = advisorRefreshTurns as number
|
|
231
|
+
if (advisorScoreThreshold !== undefined) result.advisorScoreThreshold = advisorScoreThreshold as number
|
|
232
|
+
if (advisorSampleLimit !== undefined) result.advisorSampleLimit = advisorSampleLimit as number
|
|
233
|
+
if (advisorMinTokens !== undefined) result.advisorMinTokens = advisorMinTokens as number
|
|
193
234
|
return result
|
|
194
235
|
}
|
|
195
236
|
|
|
@@ -463,6 +504,15 @@ const PRESET_OPTION_DEFAULTS: PresetOptions = deepFreeze({
|
|
|
463
504
|
reviewTimeoutTurns: 6,
|
|
464
505
|
cacheHitDiscountAlpha: 0.1,
|
|
465
506
|
reviewHighImpactTokens: 4000,
|
|
507
|
+
// Advisory advisor ships off: statistics and suggestions only, never a gate.
|
|
508
|
+
advisor: {
|
|
509
|
+
mode: '',
|
|
510
|
+
timeoutMs: 8_000,
|
|
511
|
+
refreshTurns: 8,
|
|
512
|
+
scoreThreshold: 0.35,
|
|
513
|
+
sampleLimit: 16,
|
|
514
|
+
minTokens: 250,
|
|
515
|
+
},
|
|
466
516
|
})
|
|
467
517
|
|
|
468
518
|
/**
|
|
@@ -484,6 +534,14 @@ function mergePresetOptions(overrides: PresetOptionsSettings | undefined): Prese
|
|
|
484
534
|
reviewTimeoutTurns: overrides.reviewTimeoutTurns ?? PRESET_OPTION_DEFAULTS.reviewTimeoutTurns,
|
|
485
535
|
cacheHitDiscountAlpha: overrides.cacheHitDiscountAlpha ?? PRESET_OPTION_DEFAULTS.cacheHitDiscountAlpha,
|
|
486
536
|
reviewHighImpactTokens: overrides.reviewHighImpactTokens ?? PRESET_OPTION_DEFAULTS.reviewHighImpactTokens,
|
|
537
|
+
advisor: {
|
|
538
|
+
mode: overrides.advisorMode ?? PRESET_OPTION_DEFAULTS.advisor.mode,
|
|
539
|
+
timeoutMs: overrides.advisorTimeoutMs ?? PRESET_OPTION_DEFAULTS.advisor.timeoutMs,
|
|
540
|
+
refreshTurns: overrides.advisorRefreshTurns ?? PRESET_OPTION_DEFAULTS.advisor.refreshTurns,
|
|
541
|
+
scoreThreshold: overrides.advisorScoreThreshold ?? PRESET_OPTION_DEFAULTS.advisor.scoreThreshold,
|
|
542
|
+
sampleLimit: overrides.advisorSampleLimit ?? PRESET_OPTION_DEFAULTS.advisor.sampleLimit,
|
|
543
|
+
minTokens: overrides.advisorMinTokens ?? PRESET_OPTION_DEFAULTS.advisor.minTokens,
|
|
544
|
+
},
|
|
487
545
|
})
|
|
488
546
|
}
|
|
489
547
|
|