dsh-context-compression-improved 0.3.0 → 0.4.0-beta.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/.githooks/pre-push +37 -0
- package/package.json +3 -2
- package/packages/selector/cordis.patch.yml +12 -5
- package/packages/selector/lib/client.d.ts +24 -0
- package/packages/selector/lib/client.js +506 -5
- package/packages/selector/lib/config.js +27 -4
- package/packages/selector/lib/index.d.ts +7 -0
- package/packages/selector/lib/index.js +229 -1
- package/packages/selector/lib/pruner.d.ts +254 -0
- package/packages/selector/lib/pruner.js +714 -25
- package/packages/selector/package.json +0 -1
- package/packages/selector/src/client/EstimatorControls.tsx +101 -0
- package/packages/selector/src/client/ReviewOverlay.tsx +320 -0
- package/packages/selector/src/client/index.ts +17 -0
- package/packages/selector/src/client/locales.ts +38 -0
- package/packages/selector/src/client/preset-options.ts +1 -0
- package/packages/selector/src/client/review-scope.ts +16 -0
- package/packages/selector/src/client/settings-section.tsx +17 -8
- package/packages/selector/src/index.ts +308 -0
- package/packages/selector/src/profiles.ts +28 -1
- package/packages/selector/src/pruner/state.ts +27 -0
- package/packages/selector/src/pruner.ts +430 -10
- package/packages/selector/src/runtime/audit.ts +27 -0
- package/packages/selector/src/runtime/config.ts +33 -1
- package/packages/selector/src/runtime/tokenpilot/estimator.ts +60 -13
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +223 -0
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +231 -0
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +122 -0
- package/packages/selector/src/runtime/types.ts +17 -0
- package/packages/selector/tests/code-skeleton.client.spec.ts +3 -2
- package/packages/selector/tests/custom-contract.client.spec.ts +3 -2
- package/packages/selector/tests/preset-options-write.client.spec.ts +34 -1
- package/packages/selector/tests/review-overlay.client.spec.tsx +118 -0
- package/packages/selector/tests/review-routes.host.spec.ts +290 -0
- package/packages/selector/tests/runtime/audit.spec.ts +44 -0
- package/packages/selector/tests/runtime/tokenpilot/estimator.spec.ts +23 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +5 -0
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +199 -0
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +313 -0
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +168 -0
- package/packages/selector/tests/settings-seat.client.spec.ts +5 -4
|
@@ -33,6 +33,302 @@ const ESTIMATOR_CATALOG_ROUTES = [
|
|
|
33
33
|
'/api/dsh-context-compression-improved/estimator-catalog',
|
|
34
34
|
] as const
|
|
35
35
|
|
|
36
|
+
// TokenPilot-inspired R4: the review pipeline's client↔runtime channel, dual
|
|
37
|
+
// prefixed like the catalog route so 0.1.2 clients keep working.
|
|
38
|
+
const REVIEW_QUEUE_ROUTES = [
|
|
39
|
+
'/endpoint/dsh-context-compression-improved/review-queue',
|
|
40
|
+
'/api/dsh-context-compression-improved/review-queue',
|
|
41
|
+
] as const
|
|
42
|
+
const REVIEW_DECIDE_ROUTES = [
|
|
43
|
+
'/endpoint/dsh-context-compression-improved/review-decide',
|
|
44
|
+
'/api/dsh-context-compression-improved/review-decide',
|
|
45
|
+
] as const
|
|
46
|
+
|
|
47
|
+
/** The review faces of the pruner service the routes consume. */
|
|
48
|
+
interface ReviewPrunerLike {
|
|
49
|
+
listReviewProposals(session: unknown): readonly {
|
|
50
|
+
readonly id: string
|
|
51
|
+
readonly kind: string
|
|
52
|
+
readonly items: readonly {
|
|
53
|
+
readonly seq: number
|
|
54
|
+
readonly kind: string
|
|
55
|
+
readonly component: string
|
|
56
|
+
readonly tokensBefore: number
|
|
57
|
+
readonly tokensAfter: number
|
|
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
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Minimal face of the agents service: session id → agent (carrying the session). */
|
|
94
|
+
interface AgentsServiceLike {
|
|
95
|
+
get?(id: unknown): { session?: unknown } | undefined
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function reviewPrunerOf(readService: (name: string) => unknown): ReviewPrunerLike | undefined {
|
|
99
|
+
const candidate = readService('toolResultPruner') as {
|
|
100
|
+
listReviewProposals?: unknown
|
|
101
|
+
decideReviewProposal?: unknown
|
|
102
|
+
} | undefined
|
|
103
|
+
return typeof candidate?.listReviewProposals === 'function'
|
|
104
|
+
&& typeof candidate?.decideReviewProposal === 'function'
|
|
105
|
+
? candidate as unknown as ReviewPrunerLike
|
|
106
|
+
: undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sessionFor(readService: (name: string) => unknown, sessionId: string): unknown {
|
|
110
|
+
const agents = readService('agents') as AgentsServiceLike | undefined
|
|
111
|
+
return typeof agents?.get === 'function' ? agents.get(sessionId)?.session : undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function reviewJson(res: unknown, status: number, body: unknown): void {
|
|
115
|
+
const resTyped = res as {
|
|
116
|
+
writeHead: (code: number, headers?: Record<string, string>) => void
|
|
117
|
+
end: (body?: string) => void
|
|
118
|
+
}
|
|
119
|
+
resTyped.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' })
|
|
120
|
+
resTyped.end(JSON.stringify(body))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readRequestBody(req: unknown): Promise<string> {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
const typed = req as {
|
|
126
|
+
on?: (event: string, listener: (chunk?: Buffer) => void) => void
|
|
127
|
+
}
|
|
128
|
+
let data = ''
|
|
129
|
+
try {
|
|
130
|
+
typed.on?.('data', chunk => {
|
|
131
|
+
data += String(chunk ?? '')
|
|
132
|
+
if (data.length > 64 * 1024) {
|
|
133
|
+
data = ''
|
|
134
|
+
resolve('')
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
typed.on?.('end', () => resolve(data))
|
|
138
|
+
typed.on?.('error', reject)
|
|
139
|
+
} catch (error) {
|
|
140
|
+
reject(error instanceof Error ? error : new Error(String(error)))
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Serve the review pipeline's two HTTP routes (best effort, mirroring the
|
|
147
|
+
* estimator-catalog registration):
|
|
148
|
+
*
|
|
149
|
+
* - `GET .../review-queue?sessionId=…` → the session's pending proposals with
|
|
150
|
+
* their benefit numbers. Sanitized by construction: the queue never holds
|
|
151
|
+
* message content, and the response carries ids/seqs/counts only (digests
|
|
152
|
+
* stay in the runtime — the client cannot need them).
|
|
153
|
+
* - `POST .../review-decide` `{sessionId, proposalId, decision}` → one human
|
|
154
|
+
* decision. Invalid body → 400; unknown/not-pending proposal → 404; review
|
|
155
|
+
* mode off for the session → 503.
|
|
156
|
+
*/
|
|
157
|
+
function registerReviewQueueRoutes(ctx: Context): void {
|
|
158
|
+
const readService = (name: string): unknown => {
|
|
159
|
+
try {
|
|
160
|
+
return (ctx as unknown as { get: (service: string) => unknown }).get(name)
|
|
161
|
+
} catch {
|
|
162
|
+
return undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const log = (level: 'info' | 'warn', message: string, ...args: unknown[]): void => {
|
|
166
|
+
console[level](message, ...args)
|
|
167
|
+
}
|
|
168
|
+
const registered = (): void => {
|
|
169
|
+
log('info', 'context-compression review queue routes registered: %s / %s', REVIEW_QUEUE_ROUTES.join(', '), REVIEW_DECIDE_ROUTES.join(', '))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type SanitizedProposal = {
|
|
173
|
+
sessionId: string
|
|
174
|
+
id: string
|
|
175
|
+
kind: string
|
|
176
|
+
items: readonly { seq: number, kind: string, component: string, tokensBefore: number, tokensAfter: number }[]
|
|
177
|
+
benefit: {
|
|
178
|
+
recoveredTokens: number
|
|
179
|
+
penaltyTokens: number
|
|
180
|
+
paybackTurns?: number
|
|
181
|
+
expectedSaving?: number
|
|
182
|
+
}
|
|
183
|
+
enqueuedTurn: number
|
|
184
|
+
lastTurnIndex: number
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const getHandler = (req: unknown, res: unknown): void => {
|
|
188
|
+
const pruner = reviewPrunerOf(readService)
|
|
189
|
+
if (pruner === undefined || pruner.listAllReviewProposals === undefined) {
|
|
190
|
+
reviewJson(res, 503, { ok: false, error: 'review pipeline unavailable' })
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
let sessionId = ''
|
|
194
|
+
try {
|
|
195
|
+
const url = new URL(String((req as { url?: string }).url ?? ''), 'http://localhost')
|
|
196
|
+
sessionId = url.searchParams.get('sessionId') ?? ''
|
|
197
|
+
} catch {
|
|
198
|
+
sessionId = ''
|
|
199
|
+
}
|
|
200
|
+
const sanitize = (
|
|
201
|
+
sid: string,
|
|
202
|
+
proposal: {
|
|
203
|
+
id: string
|
|
204
|
+
kind: string
|
|
205
|
+
items: readonly { seq: number, kind: string, component: string, tokensBefore: number, tokensAfter: number }[]
|
|
206
|
+
benefit: { recoveredTokens: number, penaltyTokens: number, paybackTurns?: number, expectedSaving?: number }
|
|
207
|
+
enqueuedTurn: number
|
|
208
|
+
lastTurnIndex: number
|
|
209
|
+
},
|
|
210
|
+
): SanitizedProposal => ({
|
|
211
|
+
sessionId: sid,
|
|
212
|
+
id: proposal.id,
|
|
213
|
+
kind: proposal.kind,
|
|
214
|
+
items: proposal.items.map(item => ({
|
|
215
|
+
seq: item.seq,
|
|
216
|
+
kind: item.kind,
|
|
217
|
+
component: item.component,
|
|
218
|
+
tokensBefore: item.tokensBefore,
|
|
219
|
+
tokensAfter: item.tokensAfter,
|
|
220
|
+
})),
|
|
221
|
+
benefit: {
|
|
222
|
+
recoveredTokens: proposal.benefit.recoveredTokens,
|
|
223
|
+
penaltyTokens: proposal.benefit.penaltyTokens,
|
|
224
|
+
...(proposal.benefit.paybackTurns === undefined ? {} : { paybackTurns: proposal.benefit.paybackTurns }),
|
|
225
|
+
...(proposal.benefit.expectedSaving === undefined ? {} : { expectedSaving: proposal.benefit.expectedSaving }),
|
|
226
|
+
},
|
|
227
|
+
enqueuedTurn: proposal.enqueuedTurn,
|
|
228
|
+
lastTurnIndex: proposal.lastTurnIndex,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Without a sessionId the read aggregates every session with live
|
|
232
|
+
// proposals — the client carries no session id of its own.
|
|
233
|
+
if (sessionId === '') {
|
|
234
|
+
const pending = pruner.listAllReviewProposals()
|
|
235
|
+
.flatMap(entry => entry.proposals.map(proposal => sanitize(entry.sessionId, proposal)))
|
|
236
|
+
reviewJson(res, 200, { ok: true, total: pending.length, pending })
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const session = sessionFor(readService, sessionId)
|
|
241
|
+
if (session === undefined) {
|
|
242
|
+
reviewJson(res, 404, { ok: false, error: 'unknown session' })
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
const pending = pruner.listReviewProposals(session).map(proposal => sanitize(sessionId, proposal))
|
|
246
|
+
const summary = pruner.reviewSummary?.(session)
|
|
247
|
+
reviewJson(res, 200, {
|
|
248
|
+
ok: true,
|
|
249
|
+
sessionId,
|
|
250
|
+
total: pending.length,
|
|
251
|
+
pending,
|
|
252
|
+
...(summary === undefined ? {} : { summary }),
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const decideHandler = async (req: unknown, res: unknown): Promise<void> => {
|
|
257
|
+
const pruner = reviewPrunerOf(readService)
|
|
258
|
+
if (pruner === undefined) {
|
|
259
|
+
reviewJson(res, 503, { ok: false, error: 'review pipeline unavailable' })
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
let body: unknown
|
|
263
|
+
try {
|
|
264
|
+
body = JSON.parse(await readRequestBody(req))
|
|
265
|
+
} catch {
|
|
266
|
+
body = undefined
|
|
267
|
+
}
|
|
268
|
+
if (typeof body !== 'object' || body === null) {
|
|
269
|
+
reviewJson(res, 400, { ok: false, error: 'invalid JSON body' })
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
const record = body as { sessionId?: unknown, proposalId?: unknown, decision?: unknown }
|
|
273
|
+
if (typeof record.sessionId !== 'string' || record.sessionId === ''
|
|
274
|
+
|| typeof record.proposalId !== 'string' || record.proposalId === '') {
|
|
275
|
+
reviewJson(res, 400, { ok: false, error: 'sessionId and proposalId are required' })
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
if (record.decision !== 'approved' && record.decision !== 'rejected' && record.decision !== 'ignored') {
|
|
279
|
+
reviewJson(res, 400, { ok: false, error: 'decision must be approved, rejected, or ignored' })
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
const session = sessionFor(readService, record.sessionId)
|
|
283
|
+
if (session === undefined) {
|
|
284
|
+
reviewJson(res, 404, { ok: false, error: 'unknown session' })
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
const outcome = pruner.decideReviewProposal(session, record.proposalId, record.decision)
|
|
288
|
+
if (outcome === undefined) {
|
|
289
|
+
reviewJson(res, 503, { ok: false, error: 'review mode is off for this session' })
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
if (!outcome.ok) {
|
|
293
|
+
reviewJson(res, 404, { ok: false, error: outcome.reason })
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
reviewJson(res, 200, { ok: true, sessionId: record.sessionId, proposalId: record.proposalId, decision: record.decision })
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const register = (webServer: WebServerLike): void => {
|
|
300
|
+
const table: ReadonlyArray<{ path: string, handler: (req: unknown, res: unknown) => unknown }> = [
|
|
301
|
+
...[...REVIEW_QUEUE_ROUTES].map(path => ({ path, handler: getHandler })),
|
|
302
|
+
...[...REVIEW_DECIDE_ROUTES].map(path => ({ path, handler: (req: unknown, res: unknown) => { void decideHandler(req, res) } })),
|
|
303
|
+
]
|
|
304
|
+
const disposers = table
|
|
305
|
+
.map(entry => webServer.register({ kind: 'exact', path: entry.path, handler: entry.handler }))
|
|
306
|
+
.filter((off): off is () => void => typeof off === 'function')
|
|
307
|
+
ctx.effect(
|
|
308
|
+
() => () => { for (const off of disposers) off() },
|
|
309
|
+
'contextCompressionSelector.review routes',
|
|
310
|
+
)
|
|
311
|
+
registered()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const active = asWebServer(readService('webServer'))
|
|
315
|
+
if (active !== undefined) {
|
|
316
|
+
register(active)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
ctx.inject(['webServer'], (injected) => {
|
|
321
|
+
const webServer = asWebServer((injected as { webServer?: unknown }).webServer)
|
|
322
|
+
if (webServer === undefined) {
|
|
323
|
+
log('warn', 'context-compression webServer exposes no register() — review routes not registered')
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
register(webServer)
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
log('warn', 'context-compression webServer not active yet — review routes pending: %s', REVIEW_QUEUE_ROUTES.join(', '))
|
|
330
|
+
}
|
|
331
|
+
|
|
36
332
|
/**
|
|
37
333
|
* The one service the catalog route actually needs. `llm` and
|
|
38
334
|
* `agentDefaultModel` are payload enrichment the handler resolves per request,
|
|
@@ -209,12 +505,20 @@ export interface Config {
|
|
|
209
505
|
* covers both arrival orders.
|
|
210
506
|
*/
|
|
211
507
|
estimatorCatalogRoute?: boolean
|
|
508
|
+
/**
|
|
509
|
+
* Register the review pipeline's HTTP routes (pending-queue read + decide
|
|
510
|
+
* write) on this row. Same Bundle opt-in semantics as
|
|
511
|
+
* `estimatorCatalogRoute`; without the routes the floating window has no
|
|
512
|
+
* transport and simply never appears.
|
|
513
|
+
*/
|
|
514
|
+
reviewQueueRoute?: boolean
|
|
212
515
|
}
|
|
213
516
|
|
|
214
517
|
/** Loader validation for the standalone Bundle opt-in. */
|
|
215
518
|
export const Config: z<Config> = z.object({
|
|
216
519
|
presetOverlay: z.boolean().default(false),
|
|
217
520
|
estimatorCatalogRoute: z.boolean().default(false),
|
|
521
|
+
reviewQueueRoute: z.boolean().default(false),
|
|
218
522
|
})
|
|
219
523
|
|
|
220
524
|
/** Register the persisted default read by the currently mounted root pruner. */
|
|
@@ -234,6 +538,10 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
234
538
|
// service, because the route is host-wide rather than per-pruner-instance.
|
|
235
539
|
if (config.estimatorCatalogRoute === true) registerEstimatorCatalogRoute(ctx)
|
|
236
540
|
|
|
541
|
+
// TokenPilot-inspired R4: the review pipeline's client transport (see the
|
|
542
|
+
// config JSDoc for the opt-in semantics).
|
|
543
|
+
if (config.reviewQueueRoute === true) registerReviewQueueRoutes(ctx)
|
|
544
|
+
|
|
237
545
|
if (config.presetOverlay !== true) return
|
|
238
546
|
|
|
239
547
|
ctx.inject(['agentPresets'], (presetsCtx) => {
|
|
@@ -135,6 +135,11 @@ export interface PresetOptionsSettings {
|
|
|
135
135
|
readonly estimatorBaseUrl?: string
|
|
136
136
|
readonly estimatorApiKey?: string
|
|
137
137
|
readonly estimatorTimeoutMs?: number
|
|
138
|
+
/** Review-mode overrides (beta); mirrors the runtime PresetOptions.reviewMode. */
|
|
139
|
+
readonly reviewMode?: boolean
|
|
140
|
+
readonly reviewTimeoutTurns?: number
|
|
141
|
+
readonly cacheHitDiscountAlpha?: number
|
|
142
|
+
readonly reviewHighImpactTokens?: number
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
/**
|
|
@@ -148,9 +153,10 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
148
153
|
const allowed = new Set([
|
|
149
154
|
'dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'estimatorMode',
|
|
150
155
|
'estimatorProvider', 'estimatorModel', 'estimatorBaseUrl', 'estimatorApiKey', 'estimatorTimeoutMs',
|
|
156
|
+
'reviewMode', 'reviewTimeoutTurns', 'cacheHitDiscountAlpha', 'reviewHighImpactTokens',
|
|
151
157
|
])
|
|
152
158
|
if (Object.keys(value).some(key => !allowed.has(key))) return undefined
|
|
153
|
-
for (const key of ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState'] as const) {
|
|
159
|
+
for (const key of ['dedupeToolResults', 'summaryLocator', 'prefixStabilizer', 'readState', 'reviewMode'] as const) {
|
|
154
160
|
const entry = value[key]
|
|
155
161
|
if (entry !== undefined && typeof entry !== 'boolean') return undefined
|
|
156
162
|
}
|
|
@@ -168,6 +174,23 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
168
174
|
|| estimatorTimeoutMs < 100 || estimatorTimeoutMs > 60_000)) {
|
|
169
175
|
return undefined
|
|
170
176
|
}
|
|
177
|
+
const reviewTimeoutTurns = value.reviewTimeoutTurns
|
|
178
|
+
if (reviewTimeoutTurns !== undefined
|
|
179
|
+
&& (typeof reviewTimeoutTurns !== 'number' || !Number.isSafeInteger(reviewTimeoutTurns) || reviewTimeoutTurns < 1)) {
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
const cacheHitDiscountAlpha = value.cacheHitDiscountAlpha
|
|
183
|
+
if (cacheHitDiscountAlpha !== undefined
|
|
184
|
+
&& (typeof cacheHitDiscountAlpha !== 'number' || !Number.isFinite(cacheHitDiscountAlpha)
|
|
185
|
+
|| cacheHitDiscountAlpha <= 0 || cacheHitDiscountAlpha >= 1)) {
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
const reviewHighImpactTokens = value.reviewHighImpactTokens
|
|
189
|
+
if (reviewHighImpactTokens !== undefined
|
|
190
|
+
&& (typeof reviewHighImpactTokens !== 'number' || !Number.isSafeInteger(reviewHighImpactTokens)
|
|
191
|
+
|| reviewHighImpactTokens < 0)) {
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
171
194
|
const decoded: {
|
|
172
195
|
-readonly [K in keyof PresetOptionsSettings]: PresetOptionsSettings[K]
|
|
173
196
|
} = {}
|
|
@@ -181,6 +204,10 @@ export function decodePresetOptionsSettings(value: unknown): PresetOptionsSettin
|
|
|
181
204
|
if (value.estimatorBaseUrl !== undefined) decoded.estimatorBaseUrl = value.estimatorBaseUrl as string
|
|
182
205
|
if (value.estimatorApiKey !== undefined) decoded.estimatorApiKey = value.estimatorApiKey as string
|
|
183
206
|
if (estimatorTimeoutMs !== undefined) decoded.estimatorTimeoutMs = estimatorTimeoutMs as number
|
|
207
|
+
if (value.reviewMode !== undefined) decoded.reviewMode = value.reviewMode as boolean
|
|
208
|
+
if (reviewTimeoutTurns !== undefined) decoded.reviewTimeoutTurns = reviewTimeoutTurns as number
|
|
209
|
+
if (cacheHitDiscountAlpha !== undefined) decoded.cacheHitDiscountAlpha = cacheHitDiscountAlpha as number
|
|
210
|
+
if (reviewHighImpactTokens !== undefined) decoded.reviewHighImpactTokens = reviewHighImpactTokens as number
|
|
184
211
|
return decoded
|
|
185
212
|
}
|
|
186
213
|
|
|
@@ -8,11 +8,24 @@
|
|
|
8
8
|
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
|
+
import type { ReviewQueue, ReviewQueueStore } from '../runtime/tokenpilot/review-queue.ts'
|
|
11
12
|
import type {
|
|
12
13
|
ContextCompressionSettings,
|
|
13
14
|
ResolvedConfig,
|
|
14
15
|
} from '../runtime/types.ts'
|
|
15
16
|
|
|
17
|
+
/** Four-state per-session outcome counters behind the floating-window summary row. */
|
|
18
|
+
export interface ReviewSessionSummary {
|
|
19
|
+
/** Rewrites that landed through the automatic path while review mode served this session. */
|
|
20
|
+
autoApplied: number
|
|
21
|
+
/** Approved proposals whose merged batch executed with an applied receipt. */
|
|
22
|
+
reviewApplied: number
|
|
23
|
+
/** Pending proposals that expired unhandled at a turn boundary. */
|
|
24
|
+
expired: number
|
|
25
|
+
/** Approved proposals voided at the apply point (digest mismatch et al). */
|
|
26
|
+
voided: number
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
/** Mutable per-session state bag used inside {@link ToolResultPruner}. */
|
|
17
30
|
export interface PrunerState {
|
|
18
31
|
/** Resolved immutable deployment configuration. */
|
|
@@ -40,4 +53,18 @@ export interface PrunerState {
|
|
|
40
53
|
readonly tailTrimBoundaryAttempts: WeakMap<Session, object>
|
|
41
54
|
/** Last effective policy audit key emitted for each Session. */
|
|
42
55
|
readonly policyResolutionAudits: WeakMap<Session, string>
|
|
56
|
+
/**
|
|
57
|
+
* TokenPilot-inspired R4: shared review-queue store. Starts as the in-memory
|
|
58
|
+
* fail-open fallback; swapped to the storageDomain-backed adapter when (and
|
|
59
|
+
* if) that seam opens successfully.
|
|
60
|
+
*/
|
|
61
|
+
reviewStore: ReviewQueueStore
|
|
62
|
+
/** Per-session review queue carrying the frozen timeout policy. */
|
|
63
|
+
readonly reviewQueues: WeakMap<Session, ReviewQueue>
|
|
64
|
+
/** Last observed turn index per Session: the monotonic clock for review expiries. */
|
|
65
|
+
readonly reviewClocks: WeakMap<Session, number>
|
|
66
|
+
/** Estimator-reported remaining turns Ŝ per Session; advisory only. */
|
|
67
|
+
readonly estimatorRemainingTurns: WeakMap<Session, number>
|
|
68
|
+
/** Four-state outcome counters per Session (floating-window summary row). */
|
|
69
|
+
readonly reviewSummaries: WeakMap<Session, ReviewSessionSummary>
|
|
43
70
|
}
|