dsh-context-compression-improved 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.ja.md +51 -0
  2. package/CHANGELOG.ko.md +51 -0
  3. package/CHANGELOG.md +55 -0
  4. package/CHANGELOG.zh.md +45 -0
  5. package/package.json +1 -1
  6. package/packages/selector/lib/advisor-state.js +4 -231
  7. package/packages/selector/lib/client.d.ts +0 -24
  8. package/packages/selector/lib/client.js +6 -501
  9. package/packages/selector/lib/index.d.ts +4 -10
  10. package/packages/selector/lib/index.js +65 -235
  11. package/packages/selector/lib/pruner.d.ts +13 -248
  12. package/packages/selector/lib/pruner.js +148 -552
  13. package/packages/selector/src/client/EstimatorControls.tsx +277 -378
  14. package/packages/selector/src/client/index.ts +0 -17
  15. package/packages/selector/src/client/locales.ts +196 -234
  16. package/packages/selector/src/client/preset-options.ts +3 -2
  17. package/packages/selector/src/client/settings-section.tsx +8 -17
  18. package/packages/selector/src/index.ts +463 -710
  19. package/packages/selector/src/preset-overlay.ts +60 -1
  20. package/packages/selector/src/profiles.ts +4 -27
  21. package/packages/selector/src/pruner/state.ts +50 -73
  22. package/packages/selector/src/pruner.ts +2402 -2730
  23. package/packages/selector/src/runtime/audit.ts +27 -21
  24. package/packages/selector/src/runtime/config.ts +6 -32
  25. package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +16 -0
  26. package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
  27. package/packages/selector/src/runtime/types.ts +0 -17
  28. package/packages/selector/tests/built/client-artifact.spec.ts +9 -5
  29. package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
  30. package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
  31. package/packages/selector/tests/runtime/audit.spec.ts +35 -21
  32. package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
  33. package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
  34. package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
  35. package/packages/selector/tests/settings-seat.client.spec.ts +16 -10
  36. package/packages/selector/tests/standing-generation.host.spec.ts +54 -5
  37. package/scripts/packed-components-smoke.mjs +30 -8
  38. package/scripts/packed-install-e2e.mjs +69 -15
  39. package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
  40. package/packages/selector/src/client/review-scope.ts +0 -16
  41. package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
  42. package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
  43. package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
  44. package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
  45. package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
  46. package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
  47. package/packages/selector/tests/review-routes.host.spec.ts +0 -290
  48. package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
  49. package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
  50. package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
@@ -1,382 +0,0 @@
1
- /**
2
- * TokenPilot-inspired R4 integration coverage: the human-gated review pipeline
3
- * on published Harness APIs. Mirrors the public-runtime harness (real
4
- * Session/log, exact tokenizer, host settings) and pins the four acceptance
5
- * behaviors from the phase-1 spec: approved proposals execute as one merged
6
- * batch at the turn boundary with evidence-built receipts; tampered content
7
- * voids instead of deleting; expired proposals never execute; review-off
8
- * behavior stays byte-identical to the automatic path.
9
- */
10
- import { describe, expect, it, vi } from 'vitest'
11
- import { Context } from '@deepseek-ai/cordis'
12
- import {
13
- ToolCallId as CallId,
14
- createMessage,
15
- createUserMessage,
16
- createToolResultMessage,
17
- freezeMessage,
18
- } from '@deepseek-ai/dsh-llm'
19
- import SessionStore, {
20
- Session,
21
- SessionId,
22
- SessionSeq,
23
- canonicalHeader,
24
- } from '@deepseek-ai/dsh-session'
25
- import {
26
- SettingsProvider,
27
- type SettingsNamespace,
28
- } from '@deepseek-ai/dsh-settings'
29
- import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
30
- import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
31
- import TokenMeter from '@deepseek-ai/dsh-token-meter'
32
- import ToolRuntime from '@deepseek-ai/dsh-tools'
33
- import * as SelectorHost from '../../../src/index.ts'
34
- import { sessionEvents } from '../../../src/runtime/session-events.ts'
35
- import ToolResultPruner, {
36
- CONTEXT_COMPRESSION_SETTINGS_NAMESPACE,
37
- } from '../../../src/pruner.ts'
38
- import { measureForCompaction } from '../../../src/runtime/measurement.ts'
39
- import {
40
- COMPRESSION_AUDIT_PREFIX,
41
- type CompressionAuditRecord,
42
- type CompressionRewriteAuditRecord,
43
- type ReviewOutcomeAuditRecord,
44
- } from '../../../src/runtime/audit.ts'
45
-
46
- const MODEL = 'deepseek-v4-flash'
47
-
48
- class TestSettings extends SettingsProvider {
49
- readonly writable = true
50
- private readonly stored: Record<string, unknown> = {}
51
-
52
- protected override load(): Promise<Record<string, unknown>> {
53
- return Promise.resolve(structuredClone(this.stored))
54
- }
55
-
56
- protected override persist(namespace: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
57
- this.stored[namespace] = structuredClone(section)
58
- return Promise.resolve()
59
- }
60
- }
61
-
62
- async function runtimeContext(): Promise<Context> {
63
- const ctx = new Context()
64
- await ctx.plugin(SessionStore).await()
65
- await ctx.plugin(SystemPrompt).await()
66
- await ctx.plugin(ToolRuntime).await()
67
- await ctx.plugin(SessionProjectionRegistry).await()
68
- await ctx.plugin(TokenMeter).await()
69
- return ctx
70
- }
71
-
72
- function captureAudit(ctx: Context): { records(): CompressionAuditRecord[] } {
73
- const info = vi.spyOn(ctx.logger, 'info').mockImplementation(() => ctx.logger)
74
- return {
75
- records: () => info.mock.calls.flatMap((call) => {
76
- const line = String(call[0])
77
- return line.startsWith(COMPRESSION_AUDIT_PREFIX)
78
- ? [JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord]
79
- : []
80
- }),
81
- }
82
- }
83
-
84
- function rewrites(records: readonly CompressionAuditRecord[]): CompressionRewriteAuditRecord[] {
85
- return records.filter((record): record is CompressionRewriteAuditRecord => record.kind === 'rewrite')
86
- }
87
-
88
- function reviewEvents(records: readonly CompressionAuditRecord[]): ReviewOutcomeAuditRecord[] {
89
- return records.filter((record): record is ReviewOutcomeAuditRecord => record.kind === 'review-outcome')
90
- }
91
-
92
- const nsBrand = (value: string): SettingsNamespace => value as unknown as SettingsNamespace
93
-
94
- function appendToolTurn(
95
- session: Session,
96
- turn: number,
97
- text: string,
98
- closeTurn: boolean,
99
- ): { readonly assistantSeq: number; readonly resultSeq: number } {
100
- const callId = CallId(`call-${String(turn)}`)
101
- session.append('turn/start', { turn })
102
- if (session.requestHeader() === undefined) {
103
- session.append('request/header', {
104
- reason: 'initial',
105
- header: canonicalHeader({ config: { provider: 'deepseek', model: MODEL } }),
106
- })
107
- }
108
- session.append('user/message', createUserMessage({
109
- content: [{ type: 'text', text: `user turn ${String(turn)}` }],
110
- source: { kind: 'user' },
111
- }), { surfaceOp: 'append' })
112
- session.append('step/start', { turn, step: 1 })
113
- const assistant = session.append('assistant/message', {
114
- stream: [],
115
- turn,
116
- step: 1,
117
- message: createMessage({
118
- role: 'assistant',
119
- content: [{ type: 'tool-call', id: callId, name: 'bash', arguments: '{}' }],
120
- source: { kind: 'model', provider: 'deepseek', model: MODEL },
121
- }),
122
- }, { surfaceOp: 'append' })
123
- session.append('tool/call', { turn, step: 1, callId, name: 'bash', arguments: '{}' })
124
- const result = session.append('tool/result', {
125
- turn,
126
- step: 1,
127
- message: createToolResultMessage({
128
- callId,
129
- content: [{ type: 'text', text }],
130
- isError: false,
131
- }),
132
- }, { surfaceOp: 'append' })
133
- session.append('step/end', { turn, step: 1 })
134
- if (closeTurn) session.append('turn/end', { turn, reason: { kind: 'completed' } })
135
- return { assistantSeq: assistant.seq, resultSeq: result.seq }
136
- }
137
-
138
- /** Rewrite one surface tool result in place, the way land() publishes a replacement. */
139
- function tamperResult(session: Session, seq: number, text: string): void {
140
- const event = sessionEvents(session).find(entry => entry.seq === seq)
141
- if (event?.type !== 'tool/result') throw new Error(`seq ${String(seq)} is not a tool result`)
142
- const message = event.data.message
143
- // The session accepts a surface replacement only when every non-content
144
- // field stays identical, so freeze the original message and swap the blocks.
145
- session.append('tool/result', {
146
- ...event.data,
147
- message: freezeMessage({
148
- ...message,
149
- content: [{ ...message.content[0], content: [{ type: 'text', text }] }],
150
- }),
151
- }, {
152
- surfaceOp: { op: 'replace', startSeq: SessionSeq(seq), endSeq: SessionSeq(seq) },
153
- sourceEventSeqs: [SessionSeq(seq)],
154
- })
155
- }
156
-
157
- async function reviewSetup(ctx: Context, reviewMode: boolean): Promise<void> {
158
- await ctx.plugin(TestSettings).await()
159
- await ctx.plugin(SelectorHost).await()
160
- await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
161
- profile: 'tokenpilot-inspired',
162
- ...(reviewMode
163
- ? { presetOptions: { reviewMode: true, reviewHighImpactTokens: 1 } }
164
- : {}),
165
- })
166
- await ctx.plugin(ToolResultPruner, {
167
- profile: 'tokenpilot-inspired',
168
- freshTriggerTokens: 100_000,
169
- freshTargetTokens: 90_000,
170
- aggregateTriggerTokens: 100_000,
171
- aggregateTargetTokens: 90_000,
172
- historyTriggerTokens: 400,
173
- historyKeepRecentToolCalls: 0,
174
- historyKeepRecentTokens: 1,
175
- historyMinReclaimTokens: 1,
176
- }).await()
177
- }
178
-
179
- /** One aged old result plus one protected recent result, with an open turn. */
180
- function reviewSession(ctx: Context, id: string, oldText: string): {
181
- session: Session
182
- oldResultSeq: number
183
- } {
184
- const session = Session.create(SessionId(id))
185
- const old = appendToolTurn(session, 1, oldText, true)
186
- appendToolTurn(session, 2, 'newest protected result', true)
187
- const total = measureForCompaction(ctx, session).totalTokens
188
- session.append('request/context', {
189
- provider: 'deepseek',
190
- model: MODEL,
191
- contextWindow: Math.floor(total / 0.6),
192
- })
193
- session.append('turn/start', { turn: 3 })
194
- return { session, oldResultSeq: old.resultSeq }
195
- }
196
-
197
- describe('tokenpilot review pipeline (host integration)', () => {
198
- it('withholds high-impact candidates from landing and queues them for review', async () => {
199
- const ctx = await runtimeContext()
200
- await reviewSetup(ctx, true)
201
- const audit = captureAudit(ctx)
202
- const { session, oldResultSeq } = reviewSession(ctx, 'review-queue-withhold', 'old reviewable evidence '.repeat(600))
203
-
204
- const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
205
-
206
- // The old candidate was planned but withheld: nothing landed.
207
- expect(result.pruned).toHaveLength(0)
208
- const pending = ctx.toolResultPruner.listReviewProposals(session)
209
- expect(pending).toHaveLength(1)
210
- expect(pending[0]!.status).toBe('pending')
211
- expect(pending[0]!.items[0]!.seq).toBe(oldResultSeq)
212
- expect(reviewEvents(audit.records()).filter(entry => entry.event === 'enqueue')).toHaveLength(1)
213
- // And the original content is still on the surface.
214
- expect(rewrites(audit.records())).toHaveLength(0)
215
- })
216
-
217
- it('executes an approved proposal at the turn boundary with an evidence-built receipt', async () => {
218
- const ctx = await runtimeContext()
219
- await reviewSetup(ctx, true)
220
- const audit = captureAudit(ctx)
221
- const { session } = reviewSession(ctx, 'review-apply-e2e', 'old reviewable evidence '.repeat(600))
222
- ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
223
- const pending = ctx.toolResultPruner.listReviewProposals(session)
224
- const proposalId = pending[0]!.id
225
-
226
- expect(ctx.toolResultPruner.decideReviewProposal(session, proposalId, 'approved')).toEqual({ ok: true })
227
- expect(reviewEvents(audit.records()).some(entry => entry.event === 'decide' && entry.decision === 'approved')).toBe(true)
228
-
229
- ctx.toolResultPruner.applyApprovedProposals(session)
230
-
231
- const applied = rewrites(audit.records()).filter(entry => entry.reducer === 'review-approved-whole-result')
232
- expect(applied).toHaveLength(1)
233
- expect(applied[0]!.tokensBefore).toBeGreaterThan(applied[0]!.tokensAfter)
234
- const receipts = reviewEvents(audit.records()).filter(entry => entry.event === 'apply-receipt')
235
- expect(receipts).toHaveLength(1)
236
- expect(receipts[0]!.proposalId).toBe(proposalId)
237
- expect(receipts[0]!.receiptStatus).toBe('applied')
238
- // The receipt numbers come from the executed mutation, not the estimate.
239
- expect(receipts[0]!.tokensBefore).toBe(applied[0]!.tokensBefore)
240
- expect(receipts[0]!.tokensAfter).toBe(applied[0]!.tokensAfter)
241
- // Retired from the queue: a second apply must not re-execute.
242
- expect(ctx.toolResultPruner.listReviewProposals(session)).toHaveLength(0)
243
- const before = sessionEvents(session).length
244
- ctx.toolResultPruner.applyApprovedProposals(session)
245
- expect(sessionEvents(session)).toHaveLength(before)
246
- })
247
-
248
- it('voids a proposal whose content changed between approval and the apply point', async () => {
249
- const ctx = await runtimeContext()
250
- await reviewSetup(ctx, true)
251
- const audit = captureAudit(ctx)
252
- const { session, oldResultSeq } = reviewSession(ctx, 'review-void-tamper', 'old reviewable evidence '.repeat(600))
253
- ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
254
- const proposalId = ctx.toolResultPruner.listReviewProposals(session)[0]!.id
255
- // The user approved; a later step then rewrote the same surface content.
256
- tamperResult(session, oldResultSeq, 'tampered by a later step')
257
- expect(ctx.toolResultPruner.decideReviewProposal(session, proposalId, 'approved')).toEqual({ ok: true })
258
-
259
- ctx.toolResultPruner.applyApprovedProposals(session)
260
-
261
- // No deletion: the digest mismatch voided the whole proposal.
262
- expect(rewrites(audit.records()).some(entry => entry.reducer === 'review-approved-whole-result')).toBe(false)
263
- const voids = reviewEvents(audit.records()).filter(entry => entry.event === 'apply-void')
264
- expect(voids).toHaveLength(1)
265
- expect(voids[0]!.proposalId).toBe(proposalId)
266
- expect(voids[0]!.reasonCode).toBe('review_receipt_digest_invalid')
267
- // The tampered content is untouched on the surface.
268
- const surface = sessionEvents(session).filter(entry => entry.type === 'tool/result')
269
- const last = surface.at(-1)
270
- if (last?.type !== 'tool/result') throw new Error('missing surface tool result')
271
- expect(last.data.message.content[0].content[0]).toMatchObject({ text: 'tampered by a later step' })
272
- })
273
-
274
- it('expires stale pending proposals and never executes them afterwards', async () => {
275
- const ctx = await runtimeContext()
276
- await reviewSetup(ctx, true)
277
- const audit = captureAudit(ctx)
278
- const { session } = reviewSession(ctx, 'review-expiry', 'old reviewable evidence '.repeat(600))
279
- ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
280
- const proposalId = ctx.toolResultPruner.listReviewProposals(session)[0]!.id
281
-
282
- // Within the patience window the proposal survives.
283
- expect(ctx.toolResultPruner.expireReviewProposals(session, 5)).toHaveLength(0)
284
- expect(ctx.toolResultPruner.listReviewProposals(session)).toHaveLength(1)
285
- // Past reviewTimeoutTurns (6) the pending proposal expires at the boundary.
286
- const expired = ctx.toolResultPruner.expireReviewProposals(session, 8)
287
- expect(expired.map(entry => entry.id)).toEqual([proposalId])
288
- expect(reviewEvents(audit.records()).some(entry => entry.event === 'expire')).toBe(true)
289
- expect(ctx.toolResultPruner.listReviewProposals(session)).toHaveLength(0)
290
-
291
- // An expired proposal cannot be approved and never executes.
292
- expect(ctx.toolResultPruner.decideReviewProposal(session, proposalId, 'approved'))
293
- .toEqual({ ok: false, reason: 'unknown-proposal' })
294
- ctx.toolResultPruner.applyApprovedProposals(session)
295
- expect(rewrites(audit.records()).some(entry => entry.reducer === 'review-approved-whole-result')).toBe(false)
296
- })
297
-
298
- it('keeps the automatic path byte-identical when review mode is off', async () => {
299
- const ctx = await runtimeContext()
300
- await reviewSetup(ctx, false)
301
- const audit = captureAudit(ctx)
302
- const { session } = reviewSession(ctx, 'review-off-identity', 'old reviewable evidence '.repeat(600))
303
-
304
- const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
305
-
306
- // History aged exactly as before the review pipeline existed.
307
- expect(result.pruned).toHaveLength(1)
308
- expect(rewrites(audit.records()).some(entry => entry.component === 'history')).toBe(true)
309
- expect(reviewEvents(audit.records())).toHaveLength(0)
310
- expect(ctx.toolResultPruner.listReviewProposals(session)).toHaveLength(0)
311
- })
312
- })
313
-
314
- /** Fresh-stage triage must NOT charge the tail-refill penalty: the fresh
315
- * candidate has never been served, so shaping it causes no cache break.
316
- * Fixture keeps historyKeepRecentTokens at the production-like 64,000 so
317
- * history pricing (penalty 57,600) would drop the batch outright — the
318
- * fresh stage must still land it. */
319
- describe('tokenpilot review pipeline (fresh-stage exemption)', () => {
320
- async function freshReviewSetup(ctx: Context): Promise<void> {
321
- await ctx.plugin(TestSettings).await()
322
- await ctx.plugin(SelectorHost).await()
323
- await ctx.settings.update(nsBrand(CONTEXT_COMPRESSION_SETTINGS_NAMESPACE), {
324
- profile: 'tokenpilot-inspired',
325
- presetOptions: { reviewMode: true, reviewHighImpactTokens: 1_000_000 },
326
- })
327
- await ctx.plugin(ToolResultPruner, {
328
- profile: 'tokenpilot-inspired',
329
- freshTriggerTokens: 200,
330
- freshTargetTokens: 100,
331
- aggregateTriggerTokens: 1_000_000,
332
- aggregateTargetTokens: 900_000,
333
- historyTriggerTokens: 400,
334
- historyKeepRecentToolCalls: 0,
335
- historyKeepRecentTokens: 64_000,
336
- historyMinReclaimTokens: 1,
337
- }).await()
338
- }
339
-
340
- function freshSession(ctx: Context, id: string, text: string): Session {
341
- const session = Session.create(SessionId(id))
342
- appendToolTurn(session, 1, text, true)
343
- const total = measureForCompaction(ctx, session).totalTokens
344
- session.append('request/context', {
345
- provider: 'deepseek',
346
- model: MODEL,
347
- contextWindow: Math.floor(total / 0.6),
348
- })
349
- // Fresh landing publishes a surface replacement, which requires an open turn.
350
- session.append('turn/start', { turn: 2 })
351
- return session
352
- }
353
-
354
- it('lands a fresh batch under review mode instead of dropping it', async () => {
355
- const ctx = await runtimeContext()
356
- await freshReviewSetup(ctx)
357
- const audit = captureAudit(ctx)
358
- const session = freshSession(ctx, 'fresh-review-exempt', 'fresh reviewable evidence '.repeat(400))
359
-
360
- const result = ctx.toolResultPruner.pruneSession(session, { stage: 'fresh', freshTurn: 1, freshStep: 1 })
361
-
362
- // Without the exemption the 57,600-token refill penalty prices this batch
363
- // into the drop band (payback > 3 with Ŝ unknown) and nothing lands.
364
- expect(result.pruned).toHaveLength(1)
365
- expect(rewrites(audit.records()).some(entry => entry.component === 'fresh')).toBe(true)
366
- expect(reviewEvents(audit.records()).filter(entry => entry.event === 'enqueue')).toHaveLength(0)
367
- })
368
-
369
- it('keeps the history batch priced with the refill penalty', async () => {
370
- const ctx = await runtimeContext()
371
- await freshReviewSetup(ctx)
372
- const audit = captureAudit(ctx)
373
- const { session } = reviewSession(ctx, 'history-review-priced', 'old reviewable evidence '.repeat(600))
374
-
375
- const result = ctx.toolResultPruner.pruneSession(session, { stage: 'pressure' })
376
-
377
- // History content was already served: the refill penalty is real, Ŝ is
378
- // unknown, payback > 3 → drop band → nothing lands and nothing queues.
379
- expect(result.pruned).toHaveLength(0)
380
- expect(reviewEvents(audit.records()).filter(entry => entry.event === 'enqueue')).toHaveLength(0)
381
- })
382
- })
@@ -1,168 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- MemoryReviewStore,
4
- ReviewQueue,
5
- type ReviewQueueStore,
6
- } from '../../../src/runtime/tokenpilot/review-queue.ts'
7
- import type { ProposalSkeleton } from '../../../src/runtime/tokenpilot/proposal.ts'
8
-
9
- const ITEM = {
10
- seq: 7,
11
- component: 'history',
12
- kind: 'read-state',
13
- tokensBefore: 1400,
14
- tokensAfter: 1000,
15
- digest: 'ab'.repeat(32),
16
- } as const
17
-
18
- function skeleton(overrides: Partial<ProposalSkeleton> = {}): ProposalSkeleton {
19
- return {
20
- id: overrides.id ?? 'proposal00001',
21
- kind: overrides.kind ?? 'read-state',
22
- items: overrides.items ?? [ITEM],
23
- benefit: overrides.benefit ?? {
24
- recoveredTokens: 400,
25
- penaltyTokens: 900,
26
- paybackTurns: 2.25,
27
- },
28
- }
29
- }
30
-
31
- /** Failing store: every seam failure degrades to memory, never throws. */
32
-
33
- function makeQueue(timeoutTurns = 6, store: ReviewQueueStore = new MemoryReviewStore()): ReviewQueue {
34
- return new ReviewQueue(store, { timeoutTurns })
35
- }
36
-
37
- describe('review queue', () => {
38
- it('moves a proposal through enqueue → pending → approved', () => {
39
- const queue = makeQueue()
40
- expect(queue.enqueue('s1', skeleton(), 3)).toBe(true)
41
- expect(queue.listPending('s1')).toHaveLength(1)
42
- expect(queue.listPending('s1')[0]!.items[0]!.digest).toBe(ITEM.digest)
43
- expect(queue.decide('s1', 'proposal00001', 'approved')).toEqual({ ok: true })
44
- expect(queue.listPending('s1')).toHaveLength(0)
45
- expect(queue.listApproved('s1')).toHaveLength(1)
46
- })
47
-
48
- it('supports every decision and keeps sessions isolated', () => {
49
- const queue = makeQueue()
50
- queue.enqueue('s1', skeleton({ id: 'p-rejected000' }), 1)
51
- queue.enqueue('s2', skeleton({ id: 'p-ignored0000' }), 1)
52
- expect(queue.decide('s1', 'p-rejected000', 'rejected')).toEqual({ ok: true })
53
- expect(queue.decide('s2', 'p-ignored0000', 'ignored')).toEqual({ ok: true })
54
- expect(queue.listPending('s1')).toHaveLength(0)
55
- expect(queue.listPending('s2')).toHaveLength(0)
56
- expect(queue.listApproved('s1')).toHaveLength(0)
57
- })
58
-
59
- it('re-enqueue of identical content refreshes patience without duplicating', () => {
60
- const queue = makeQueue()
61
- expect(queue.enqueue('s1', skeleton(), 2)).toBe(true)
62
- expect(queue.enqueue('s1', skeleton(), 5)).toBe(false)
63
- const pending = queue.listPending('s1')
64
- expect(pending).toHaveLength(1)
65
- // Patience clock restarted at the re-enqueue turn.
66
- expect(pending[0]!.lastTurnIndex).toBe(5)
67
- expect(pending[0]!.enqueuedTurn).toBe(2)
68
- })
69
-
70
- it('decide is idempotent and reports unknown or already-decided ids', () => {
71
- const queue = makeQueue()
72
- queue.enqueue('s1', skeleton(), 1)
73
- expect(queue.decide('s1', 'proposal00001', 'approved')).toEqual({ ok: true })
74
- expect(queue.decide('s1', 'proposal00001', 'approved')).toEqual({ ok: false, reason: 'not-pending' })
75
- expect(queue.decide('s1', 'missing-proposal', 'rejected')).toEqual({ ok: false, reason: 'unknown-proposal' })
76
- expect(queue.listApproved('s1')).toHaveLength(1)
77
- })
78
-
79
- it('expires only pending proposals past the timeout window at the turn boundary', () => {
80
- const queue = makeQueue(6)
81
- queue.enqueue('s1', skeleton({ id: 'p-pending-old0' }), 1)
82
- queue.enqueue('s1', skeleton({ id: 'p-pending-new0' }), 9)
83
- queue.enqueue('s1', skeleton({ id: 'p-approved-00' }), 1)
84
- queue.decide('s1', 'p-approved-00', 'approved')
85
- // Turn 8: the turn-1 pending proposal is 7 turns stale (> 6) → expired;
86
- // the approved one never expires and the turn-9 one is still fresh.
87
- const expired = queue.expireTurn('s1', 8)
88
- expect(expired.map(entry => entry.id)).toEqual(['p-pending-old0'])
89
- expect(expired[0]!.status).toBe('expired')
90
- expect(queue.listPending('s1').map(entry => entry.id)).toEqual(['p-pending-new0'])
91
- expect(queue.listApproved('s1').map(entry => entry.id)).toEqual(['p-approved-00'])
92
- })
93
-
94
- it('keeps proposals exactly at the timeout boundary', () => {
95
- const queue = makeQueue(6)
96
- queue.enqueue('s1', skeleton(), 1)
97
- // 6 - 1 = 5 ≤ 6 → still pending.
98
- expect(queue.expireTurn('s1', 6)).toHaveLength(0)
99
- expect(queue.listPending('s1')).toHaveLength(1)
100
- // 7 - 1 = 6 ≤ 6 → still pending.
101
- expect(queue.expireTurn('s1', 7)).toHaveLength(0)
102
- // 8 - 1 = 7 > 6 → expired.
103
- expect(queue.expireTurn('s1', 8)).toHaveLength(1)
104
- })
105
-
106
- it('retires approved proposals with their receipt exactly once', () => {
107
- const queue = makeQueue()
108
- queue.enqueue('s1', skeleton({ id: 'p-apply-me000' }), 1)
109
- queue.decide('s1', 'p-apply-me000', 'approved')
110
- const settled = queue.recordReceipt('s1', 'p-apply-me000', {
111
- status: 'applied',
112
- estimatedTokens: 400,
113
- appliedTokens: 380,
114
- updatedAt: '2026-09-18T00:00:00.000Z',
115
- })
116
- expect(settled).toBeDefined()
117
- expect(settled!.receipt.appliedTokens).toBe(380)
118
- expect(settled!.items).toHaveLength(1)
119
- // Retired: a second receipt for the same id misses.
120
- expect(queue.recordReceipt('s1', 'p-apply-me000', {
121
- status: 'applied',
122
- estimatedTokens: 400,
123
- updatedAt: '2026-09-18T00:00:01.000Z',
124
- })).toBeUndefined()
125
- expect(queue.listApproved('s1')).toHaveLength(0)
126
- })
127
-
128
- it('refuses a receipt for a proposal that is not approved', () => {
129
- const queue = makeQueue()
130
- queue.enqueue('s1', skeleton(), 1)
131
- expect(queue.recordReceipt('s1', 'proposal00001', {
132
- status: 'applied',
133
- estimatedTokens: 400,
134
- updatedAt: '2026-09-18T00:00:00.000Z',
135
- })).toBeUndefined()
136
- })
137
-
138
- it('never throws when the store seam fails (fail-open degrade)', () => {
139
- // When the storageDomain seam is unavailable the wiring layer may hand the
140
- // queue a store whose calls throw; the queue must degrade to no-durability
141
- // behavior without ever breaking the compression pipeline.
142
- const throwing: ReviewQueueStore = {
143
- load: () => { throw new Error('seam unavailable') },
144
- save: () => { throw new Error('seam unavailable') },
145
- }
146
- const queue = new ReviewQueue(throwing, { timeoutTurns: 6 })
147
- expect(queue.enqueue('s1', skeleton(), 1)).toBe(true)
148
- expect(queue.listPending('s1')).toEqual([])
149
- expect(queue.expireTurn('s1', 3)).toEqual([])
150
- expect(queue.decide('s1', 'proposal00001', 'approved')).toEqual({ ok: false, reason: 'unknown-proposal' })
151
- expect(queue.recordReceipt('s1', 'proposal00001', {
152
- status: 'deferred',
153
- reasonCode: 'review_receipt_digest_invalid',
154
- estimatedTokens: 400,
155
- updatedAt: '2026-09-18T00:00:00.000Z',
156
- })).toBeUndefined()
157
- })
158
-
159
- it('persists across calls through the store (memory store roundtrip)', () => {
160
- const shared = new MemoryReviewStore()
161
- const writer = makeQueue(6, shared)
162
- const reader = makeQueue(6, shared)
163
- writer.enqueue('s1', skeleton(), 1)
164
- expect(reader.listPending('s1')).toHaveLength(1)
165
- reader.decide('s1', 'proposal00001', 'approved')
166
- expect(writer.listApproved('s1')).toHaveLength(1)
167
- })
168
- })