dsh-context-compression-improved 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitattributes +1 -0
- package/CHANGELOG.ja.md +144 -83
- package/CHANGELOG.ko.md +143 -82
- package/CHANGELOG.md +278 -212
- package/CHANGELOG.zh.md +131 -77
- package/docs/installation.md +103 -103
- package/docs/installation.zh.md +100 -100
- package/package.json +1 -1
- package/packages/selector/cordis.patch.yml +5 -6
- package/packages/selector/lib/advisor-state.js +4 -231
- package/packages/selector/lib/client.d.ts +0 -24
- package/packages/selector/lib/client.js +6 -501
- package/packages/selector/lib/index.d.ts +4 -10
- package/packages/selector/lib/index.js +16 -234
- package/packages/selector/lib/pruner.d.ts +13 -248
- package/packages/selector/lib/pruner.js +148 -552
- package/packages/selector/src/client/EstimatorControls.tsx +0 -101
- package/packages/selector/src/client/index.ts +0 -17
- package/packages/selector/src/client/locales.ts +0 -38
- package/packages/selector/src/client/preset-options.ts +3 -2
- package/packages/selector/src/client/settings-section.tsx +8 -17
- package/packages/selector/src/index.ts +24 -271
- package/packages/selector/src/profiles.ts +4 -27
- package/packages/selector/src/pruner/state.ts +2 -25
- package/packages/selector/src/pruner.ts +75 -403
- package/packages/selector/src/runtime/audit.ts +27 -21
- package/packages/selector/src/runtime/config.ts +6 -32
- package/packages/selector/src/runtime/tokenpilot/advisor-prompt.ts +188 -188
- package/packages/selector/src/runtime/tokenpilot/advisor-state.ts +149 -133
- package/packages/selector/src/runtime/tokenpilot/advisor.ts +419 -419
- package/packages/selector/src/runtime/tokenpilot/benefit.ts +200 -0
- package/packages/selector/src/runtime/types.ts +0 -17
- package/packages/selector/tests/advisor-report.host.spec.ts +223 -223
- package/packages/selector/tests/preset-options-write.client.spec.ts +7 -23
- package/packages/selector/tests/public/package-contract.client.spec.ts +20 -0
- package/packages/selector/tests/runtime/advice-never-withholds.host.spec.ts +232 -0
- package/packages/selector/tests/runtime/advisor-invariant.spec.ts +272 -272
- package/packages/selector/tests/runtime/advisor.spec.ts +226 -226
- package/packages/selector/tests/runtime/audit.spec.ts +35 -21
- package/packages/selector/tests/runtime/char-basis.spec.ts +30 -30
- package/packages/selector/tests/runtime/deprecated-preset-options.spec.ts +96 -0
- package/packages/selector/tests/runtime/tokenpilot/benefit.spec.ts +217 -0
- package/packages/selector/tests/runtime/tokenpilot/profile-baseline.spec.ts +4 -5
- package/packages/selector/tests/settings-seat.client.spec.ts +45 -14
- package/scripts/toolclass-corpus-replay.mjs +281 -281
- package/packages/selector/src/client/ReviewOverlay.tsx +0 -320
- package/packages/selector/src/client/review-scope.ts +0 -16
- package/packages/selector/src/runtime/tokenpilot/proposal.ts +0 -267
- package/packages/selector/src/runtime/tokenpilot/review-queue.ts +0 -231
- package/packages/selector/src/runtime/tokenpilot/review-registry.ts +0 -117
- package/packages/selector/src/runtime/tokenpilot/review-storage.ts +0 -122
- package/packages/selector/tests/review-overlay.client.spec.tsx +0 -118
- package/packages/selector/tests/review-routes-registry.host.spec.ts +0 -142
- package/packages/selector/tests/review-routes.host.spec.ts +0 -290
- package/packages/selector/tests/runtime/tokenpilot/proposal.spec.ts +0 -393
- package/packages/selector/tests/runtime/tokenpilot/pruner-review.spec.ts +0 -382
- package/packages/selector/tests/runtime/tokenpilot/review-queue.spec.ts +0 -168
|
@@ -1,226 +1,226 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
3
|
-
import {
|
|
4
|
-
collectTaskSemantics,
|
|
5
|
-
prefixDecay,
|
|
6
|
-
selectScoringCandidates,
|
|
7
|
-
type AdvisorCandidate,
|
|
8
|
-
} from '../../src/runtime/tokenpilot/advisor.ts'
|
|
9
|
-
import {
|
|
10
|
-
ADVISOR_RECERTIFIED_LIMIT,
|
|
11
|
-
ADVISOR_SCORES_LIMIT,
|
|
12
|
-
getAdvisorState,
|
|
13
|
-
invalidateOnTaskChange,
|
|
14
|
-
recordRecertified,
|
|
15
|
-
recordScore,
|
|
16
|
-
} from '../../src/runtime/tokenpilot/advisor-state.ts'
|
|
17
|
-
import {
|
|
18
|
-
buildAdvisorScoringUserPrompt,
|
|
19
|
-
parseAdvisorScores,
|
|
20
|
-
parseAdvisorSummary,
|
|
21
|
-
} from '../../src/runtime/tokenpilot/advisor-prompt.ts'
|
|
22
|
-
|
|
23
|
-
function todoWriteEvent(data: unknown): SessionEvent {
|
|
24
|
-
return { type: 'todo/write', seq: 1, time: 0, data } as unknown as SessionEvent
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function userMessageEvent(text: string): SessionEvent {
|
|
28
|
-
return {
|
|
29
|
-
type: 'user/message',
|
|
30
|
-
seq: 2,
|
|
31
|
-
time: 0,
|
|
32
|
-
data: { content: [{ type: 'text', text }] },
|
|
33
|
-
} as unknown as SessionEvent
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
describe('collectTaskSemantics (K5)', () => {
|
|
37
|
-
it('parses a structured todo/write payload', () => {
|
|
38
|
-
const semantics = collectTaskSemantics([
|
|
39
|
-
todoWriteEvent({ todos: [{ content: 'migrate gates', status: 'in_progress' }, 'write tests'] }),
|
|
40
|
-
])
|
|
41
|
-
expect(semantics).toBeDefined()
|
|
42
|
-
expect(semantics?.source).toBe('todos')
|
|
43
|
-
expect(semantics?.taskText).toContain('migrate gates')
|
|
44
|
-
expect(semantics?.taskText).toContain('write tests')
|
|
45
|
-
// Same content ⇒ same version token (deterministic).
|
|
46
|
-
const again = collectTaskSemantics([todoWriteEvent({ todos: [{ content: 'migrate gates', status: 'in_progress' }, 'write tests'] })])
|
|
47
|
-
expect(again?.todoVersion).toBe(semantics?.todoVersion)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('degrades a malformed todo payload to its raw JSON string', () => {
|
|
51
|
-
const semantics = collectTaskSemantics([todoWriteEvent({ todos: 'not-an-array' })])
|
|
52
|
-
expect(semantics).toBeDefined()
|
|
53
|
-
expect(semantics?.source).toBe('raw-todo')
|
|
54
|
-
expect(semantics?.taskText).toContain('not-an-array')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
it('falls back to recent user/message text when no todo/write exists', () => {
|
|
58
|
-
const semantics = collectTaskSemantics([
|
|
59
|
-
userMessageEvent('earlier request'),
|
|
60
|
-
userMessageEvent('please refactor the pruner gates'),
|
|
61
|
-
])
|
|
62
|
-
expect(semantics).toBeDefined()
|
|
63
|
-
expect(semantics?.source).toBe('messages')
|
|
64
|
-
expect(semantics?.taskText).toContain('refactor the pruner gates')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('picks the most recent todo/write event', () => {
|
|
68
|
-
const semantics = collectTaskSemantics([
|
|
69
|
-
todoWriteEvent({ todos: ['old task'] }),
|
|
70
|
-
userMessageEvent('something else'),
|
|
71
|
-
todoWriteEvent({ todos: ['new task'] }),
|
|
72
|
-
])
|
|
73
|
-
expect(semantics?.taskText).toBe('new task')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it('returns undefined for an empty log', () => {
|
|
77
|
-
expect(collectTaskSemantics([])).toBeUndefined()
|
|
78
|
-
})
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe('prefixDecay (K6)', () => {
|
|
82
|
-
const candidates: AdvisorCandidate[] = [
|
|
83
|
-
{ seq: 1, characterPressure: 3_000, preview: 'a' },
|
|
84
|
-
{ seq: 2, characterPressure: 1_000, preview: 'b' },
|
|
85
|
-
]
|
|
86
|
-
// prefixDecay takes the weight face only; previews are scoring-prompt inputs.
|
|
87
|
-
const weights = candidates.map(({ seq, characterPressure }) => ({ seq, characterPressure }))
|
|
88
|
-
|
|
89
|
-
it('is deterministic and weights by character pressure', () => {
|
|
90
|
-
const scores = new Map([[1, { score: 0 }], [2, { score: 1 }]])
|
|
91
|
-
const first = prefixDecay(weights, scores)
|
|
92
|
-
const second = prefixDecay(weights, scores)
|
|
93
|
-
expect(first).toEqual(second)
|
|
94
|
-
// weight 3000×0 + 1000×1 over 4000 ⇒ relevance 0.25 ⇒ decay 0.75.
|
|
95
|
-
expect(first.decay).toBeCloseTo(0.75, 12)
|
|
96
|
-
expect(first.weightedChars).toBe(4_000)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
it('counts unscored candidates as neutral 0.5', () => {
|
|
100
|
-
const decay = prefixDecay(weights, new Map())
|
|
101
|
-
expect(decay.decay).toBeCloseTo(0.5, 12)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
it('is 0 with no candidates and ignores zero-pressure candidates', () => {
|
|
105
|
-
expect(prefixDecay([], new Map()).decay).toBe(0)
|
|
106
|
-
expect(prefixDecay([{ seq: 9, characterPressure: 0 }], new Map()).decay).toBe(0)
|
|
107
|
-
})
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
describe('selectScoringCandidates', () => {
|
|
111
|
-
const candidates: AdvisorCandidate[] = [
|
|
112
|
-
{ seq: 1, characterPressure: 8_000, preview: 'auth module login handling' },
|
|
113
|
-
{ seq: 2, characterPressure: 2_000, preview: 'tiny fragment' },
|
|
114
|
-
{ seq: 3, characterPressure: 9_000, preview: 'login auth token refresh' },
|
|
115
|
-
{ seq: 4, characterPressure: 12_000, preview: 'unrelated weather report' },
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
it('applies the watermark, the character floor, and the sample limit', () => {
|
|
119
|
-
const state = { watermarkSeq: 1 }
|
|
120
|
-
const picked = selectScoringCandidates(candidates, state, {
|
|
121
|
-
taskKeywords: new Set(['login', 'auth', 'token']),
|
|
122
|
-
minChars: 4_000,
|
|
123
|
-
sampleLimit: 1,
|
|
124
|
-
taskChanged: false,
|
|
125
|
-
})
|
|
126
|
-
// seq 2 below floor, seq 1 at/below watermark; overlap ranks seq 3 first.
|
|
127
|
-
expect(picked.map(item => item.seq)).toEqual([3])
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
it('ignores the watermark when the task semantics changed', () => {
|
|
131
|
-
const picked = selectScoringCandidates(candidates, { watermarkSeq: 4 }, {
|
|
132
|
-
taskKeywords: new Set(['login']),
|
|
133
|
-
minChars: 4_000,
|
|
134
|
-
sampleLimit: 16,
|
|
135
|
-
taskChanged: true,
|
|
136
|
-
})
|
|
137
|
-
// Overlap ties (seq 1 and 3 both match "login") break by character pressure.
|
|
138
|
-
expect(picked.map(item => item.seq)).toEqual([3, 1, 4])
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('advisor-state bounds (K8)', () => {
|
|
143
|
-
it('evicts the oldest score beyond the LRU limit', () => {
|
|
144
|
-
const session = {} as Parameters<typeof getAdvisorState>[0]
|
|
145
|
-
const state = getAdvisorState(session)
|
|
146
|
-
for (let seq = 0; seq < ADVISOR_SCORES_LIMIT + 10; seq += 1) {
|
|
147
|
-
recordScore(state, seq, { score: 0.5, turn: seq })
|
|
148
|
-
}
|
|
149
|
-
expect(state.scores.size).toBe(ADVISOR_SCORES_LIMIT)
|
|
150
|
-
expect(state.scores.has(0)).toBe(false)
|
|
151
|
-
expect(state.scores.has(ADVISOR_SCORES_LIMIT + 9)).toBe(true)
|
|
152
|
-
// Re-touching a seq moves it to the newest position.
|
|
153
|
-
recordScore(state, 10, { score: 0.9, turn: 999 })
|
|
154
|
-
recordScore(state, ADVISOR_SCORES_LIMIT + 10, { score: 0.1, turn: 1_000 })
|
|
155
|
-
expect(state.scores.has(10)).toBe(true)
|
|
156
|
-
expect(state.scores.size).toBe(ADVISOR_SCORES_LIMIT)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
it('bounds recertified marks the same way', () => {
|
|
160
|
-
const session = {} as Parameters<typeof getAdvisorState>[0]
|
|
161
|
-
const state = getAdvisorState(session)
|
|
162
|
-
for (let seq = 0; seq < ADVISOR_RECERTIFIED_LIMIT + 5; seq += 1) {
|
|
163
|
-
recordRecertified(state, seq, seq)
|
|
164
|
-
}
|
|
165
|
-
expect(state.recertified.size).toBe(ADVISOR_RECERTIFIED_LIMIT)
|
|
166
|
-
expect(state.recertified.has(0)).toBe(false)
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
it('invalidates the summary when the task version changes', () => {
|
|
170
|
-
const session = {} as Parameters<typeof getAdvisorState>[0]
|
|
171
|
-
const state = getAdvisorState(session)
|
|
172
|
-
state.summary = { overallTask: 'x', activeSubtasks: [], keywords: [], todoVersion: 'aaa', turn: 1 }
|
|
173
|
-
state.lastSummaryTurn = 1
|
|
174
|
-
expect(invalidateOnTaskChange(state, 'bbb')).toBe(true)
|
|
175
|
-
expect(state.summary).toBeUndefined()
|
|
176
|
-
expect(state.lastSummaryTurn).toBe(-1)
|
|
177
|
-
expect(invalidateOnTaskChange(state, 'bbb')).toBe(false)
|
|
178
|
-
})
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
describe('advisor prompts (K7)', () => {
|
|
182
|
-
it('parses a well-formed summary answer', () => {
|
|
183
|
-
const parsed = parseAdvisorSummary(
|
|
184
|
-
'Sure! {"overallTask":"migrate gates","activeSubtasks":["port fresh gate"],"keywords":["gates","pruner"]}',
|
|
185
|
-
)
|
|
186
|
-
expect(parsed?.overallTask).toBe('migrate gates')
|
|
187
|
-
expect(parsed?.keywords).toEqual(['gates', 'pruner'])
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('fails open on a malformed summary answer', () => {
|
|
191
|
-
expect(parseAdvisorSummary(undefined)).toBeUndefined()
|
|
192
|
-
expect(parseAdvisorSummary('')).toBeUndefined()
|
|
193
|
-
expect(parseAdvisorSummary('not json at all')).toBeUndefined()
|
|
194
|
-
expect(parseAdvisorSummary('{"overallTask":""}')).toBeUndefined()
|
|
195
|
-
expect(parseAdvisorSummary('{"overallTask":"x"}')).toBeUndefined()
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('parses JSON-lines scoring answers and drops invalid rows', () => {
|
|
199
|
-
const parsed = parseAdvisorScores(
|
|
200
|
-
'{"seq":1,"score":0.9,"reason":"current task"}\n'
|
|
201
|
-
+ 'noise line {"seq":2,"score":1.5}\n'
|
|
202
|
-
+ '{"seq":99,"score":0.5}\n'
|
|
203
|
-
+ '{"seq":3,"score":0.1}\n',
|
|
204
|
-
new Set([1, 2, 3]),
|
|
205
|
-
)
|
|
206
|
-
expect(parsed?.size).toBe(2)
|
|
207
|
-
expect(parsed?.get(1)?.score).toBe(0.9)
|
|
208
|
-
expect(parsed?.get(3)?.score).toBe(0.1)
|
|
209
|
-
expect(parsed?.has(2)).toBe(false)
|
|
210
|
-
expect(parsed?.has(99)).toBe(false)
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('fails open on empty or all-garbage scoring answers', () => {
|
|
214
|
-
expect(parseAdvisorScores(undefined, new Set([1]))).toBeUndefined()
|
|
215
|
-
expect(parseAdvisorScores('', new Set([1]))).toBeUndefined()
|
|
216
|
-
expect(parseAdvisorScores('garbage only', new Set([1]))).toBeUndefined()
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('builds a scoring prompt that keeps previews one-per-line', () => {
|
|
220
|
-
const prompt = buildAdvisorScoringUserPrompt('task text', ['sub'], [
|
|
221
|
-
{ seq: 7, preview: 'some\npreview' },
|
|
222
|
-
])
|
|
223
|
-
expect(prompt).toContain('task: task text')
|
|
224
|
-
expect(prompt).toContain('seq=7 | some preview')
|
|
225
|
-
})
|
|
226
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
|
3
|
+
import {
|
|
4
|
+
collectTaskSemantics,
|
|
5
|
+
prefixDecay,
|
|
6
|
+
selectScoringCandidates,
|
|
7
|
+
type AdvisorCandidate,
|
|
8
|
+
} from '../../src/runtime/tokenpilot/advisor.ts'
|
|
9
|
+
import {
|
|
10
|
+
ADVISOR_RECERTIFIED_LIMIT,
|
|
11
|
+
ADVISOR_SCORES_LIMIT,
|
|
12
|
+
getAdvisorState,
|
|
13
|
+
invalidateOnTaskChange,
|
|
14
|
+
recordRecertified,
|
|
15
|
+
recordScore,
|
|
16
|
+
} from '../../src/runtime/tokenpilot/advisor-state.ts'
|
|
17
|
+
import {
|
|
18
|
+
buildAdvisorScoringUserPrompt,
|
|
19
|
+
parseAdvisorScores,
|
|
20
|
+
parseAdvisorSummary,
|
|
21
|
+
} from '../../src/runtime/tokenpilot/advisor-prompt.ts'
|
|
22
|
+
|
|
23
|
+
function todoWriteEvent(data: unknown): SessionEvent {
|
|
24
|
+
return { type: 'todo/write', seq: 1, time: 0, data } as unknown as SessionEvent
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function userMessageEvent(text: string): SessionEvent {
|
|
28
|
+
return {
|
|
29
|
+
type: 'user/message',
|
|
30
|
+
seq: 2,
|
|
31
|
+
time: 0,
|
|
32
|
+
data: { content: [{ type: 'text', text }] },
|
|
33
|
+
} as unknown as SessionEvent
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('collectTaskSemantics (K5)', () => {
|
|
37
|
+
it('parses a structured todo/write payload', () => {
|
|
38
|
+
const semantics = collectTaskSemantics([
|
|
39
|
+
todoWriteEvent({ todos: [{ content: 'migrate gates', status: 'in_progress' }, 'write tests'] }),
|
|
40
|
+
])
|
|
41
|
+
expect(semantics).toBeDefined()
|
|
42
|
+
expect(semantics?.source).toBe('todos')
|
|
43
|
+
expect(semantics?.taskText).toContain('migrate gates')
|
|
44
|
+
expect(semantics?.taskText).toContain('write tests')
|
|
45
|
+
// Same content ⇒ same version token (deterministic).
|
|
46
|
+
const again = collectTaskSemantics([todoWriteEvent({ todos: [{ content: 'migrate gates', status: 'in_progress' }, 'write tests'] })])
|
|
47
|
+
expect(again?.todoVersion).toBe(semantics?.todoVersion)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('degrades a malformed todo payload to its raw JSON string', () => {
|
|
51
|
+
const semantics = collectTaskSemantics([todoWriteEvent({ todos: 'not-an-array' })])
|
|
52
|
+
expect(semantics).toBeDefined()
|
|
53
|
+
expect(semantics?.source).toBe('raw-todo')
|
|
54
|
+
expect(semantics?.taskText).toContain('not-an-array')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('falls back to recent user/message text when no todo/write exists', () => {
|
|
58
|
+
const semantics = collectTaskSemantics([
|
|
59
|
+
userMessageEvent('earlier request'),
|
|
60
|
+
userMessageEvent('please refactor the pruner gates'),
|
|
61
|
+
])
|
|
62
|
+
expect(semantics).toBeDefined()
|
|
63
|
+
expect(semantics?.source).toBe('messages')
|
|
64
|
+
expect(semantics?.taskText).toContain('refactor the pruner gates')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('picks the most recent todo/write event', () => {
|
|
68
|
+
const semantics = collectTaskSemantics([
|
|
69
|
+
todoWriteEvent({ todos: ['old task'] }),
|
|
70
|
+
userMessageEvent('something else'),
|
|
71
|
+
todoWriteEvent({ todos: ['new task'] }),
|
|
72
|
+
])
|
|
73
|
+
expect(semantics?.taskText).toBe('new task')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns undefined for an empty log', () => {
|
|
77
|
+
expect(collectTaskSemantics([])).toBeUndefined()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('prefixDecay (K6)', () => {
|
|
82
|
+
const candidates: AdvisorCandidate[] = [
|
|
83
|
+
{ seq: 1, characterPressure: 3_000, preview: 'a' },
|
|
84
|
+
{ seq: 2, characterPressure: 1_000, preview: 'b' },
|
|
85
|
+
]
|
|
86
|
+
// prefixDecay takes the weight face only; previews are scoring-prompt inputs.
|
|
87
|
+
const weights = candidates.map(({ seq, characterPressure }) => ({ seq, characterPressure }))
|
|
88
|
+
|
|
89
|
+
it('is deterministic and weights by character pressure', () => {
|
|
90
|
+
const scores = new Map([[1, { score: 0 }], [2, { score: 1 }]])
|
|
91
|
+
const first = prefixDecay(weights, scores)
|
|
92
|
+
const second = prefixDecay(weights, scores)
|
|
93
|
+
expect(first).toEqual(second)
|
|
94
|
+
// weight 3000×0 + 1000×1 over 4000 ⇒ relevance 0.25 ⇒ decay 0.75.
|
|
95
|
+
expect(first.decay).toBeCloseTo(0.75, 12)
|
|
96
|
+
expect(first.weightedChars).toBe(4_000)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('counts unscored candidates as neutral 0.5', () => {
|
|
100
|
+
const decay = prefixDecay(weights, new Map())
|
|
101
|
+
expect(decay.decay).toBeCloseTo(0.5, 12)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('is 0 with no candidates and ignores zero-pressure candidates', () => {
|
|
105
|
+
expect(prefixDecay([], new Map()).decay).toBe(0)
|
|
106
|
+
expect(prefixDecay([{ seq: 9, characterPressure: 0 }], new Map()).decay).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('selectScoringCandidates', () => {
|
|
111
|
+
const candidates: AdvisorCandidate[] = [
|
|
112
|
+
{ seq: 1, characterPressure: 8_000, preview: 'auth module login handling' },
|
|
113
|
+
{ seq: 2, characterPressure: 2_000, preview: 'tiny fragment' },
|
|
114
|
+
{ seq: 3, characterPressure: 9_000, preview: 'login auth token refresh' },
|
|
115
|
+
{ seq: 4, characterPressure: 12_000, preview: 'unrelated weather report' },
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
it('applies the watermark, the character floor, and the sample limit', () => {
|
|
119
|
+
const state = { watermarkSeq: 1 }
|
|
120
|
+
const picked = selectScoringCandidates(candidates, state, {
|
|
121
|
+
taskKeywords: new Set(['login', 'auth', 'token']),
|
|
122
|
+
minChars: 4_000,
|
|
123
|
+
sampleLimit: 1,
|
|
124
|
+
taskChanged: false,
|
|
125
|
+
})
|
|
126
|
+
// seq 2 below floor, seq 1 at/below watermark; overlap ranks seq 3 first.
|
|
127
|
+
expect(picked.map(item => item.seq)).toEqual([3])
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('ignores the watermark when the task semantics changed', () => {
|
|
131
|
+
const picked = selectScoringCandidates(candidates, { watermarkSeq: 4 }, {
|
|
132
|
+
taskKeywords: new Set(['login']),
|
|
133
|
+
minChars: 4_000,
|
|
134
|
+
sampleLimit: 16,
|
|
135
|
+
taskChanged: true,
|
|
136
|
+
})
|
|
137
|
+
// Overlap ties (seq 1 and 3 both match "login") break by character pressure.
|
|
138
|
+
expect(picked.map(item => item.seq)).toEqual([3, 1, 4])
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('advisor-state bounds (K8)', () => {
|
|
143
|
+
it('evicts the oldest score beyond the LRU limit', () => {
|
|
144
|
+
const session = {} as Parameters<typeof getAdvisorState>[0]
|
|
145
|
+
const state = getAdvisorState(session)
|
|
146
|
+
for (let seq = 0; seq < ADVISOR_SCORES_LIMIT + 10; seq += 1) {
|
|
147
|
+
recordScore(state, seq, { score: 0.5, turn: seq })
|
|
148
|
+
}
|
|
149
|
+
expect(state.scores.size).toBe(ADVISOR_SCORES_LIMIT)
|
|
150
|
+
expect(state.scores.has(0)).toBe(false)
|
|
151
|
+
expect(state.scores.has(ADVISOR_SCORES_LIMIT + 9)).toBe(true)
|
|
152
|
+
// Re-touching a seq moves it to the newest position.
|
|
153
|
+
recordScore(state, 10, { score: 0.9, turn: 999 })
|
|
154
|
+
recordScore(state, ADVISOR_SCORES_LIMIT + 10, { score: 0.1, turn: 1_000 })
|
|
155
|
+
expect(state.scores.has(10)).toBe(true)
|
|
156
|
+
expect(state.scores.size).toBe(ADVISOR_SCORES_LIMIT)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('bounds recertified marks the same way', () => {
|
|
160
|
+
const session = {} as Parameters<typeof getAdvisorState>[0]
|
|
161
|
+
const state = getAdvisorState(session)
|
|
162
|
+
for (let seq = 0; seq < ADVISOR_RECERTIFIED_LIMIT + 5; seq += 1) {
|
|
163
|
+
recordRecertified(state, seq, seq)
|
|
164
|
+
}
|
|
165
|
+
expect(state.recertified.size).toBe(ADVISOR_RECERTIFIED_LIMIT)
|
|
166
|
+
expect(state.recertified.has(0)).toBe(false)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('invalidates the summary when the task version changes', () => {
|
|
170
|
+
const session = {} as Parameters<typeof getAdvisorState>[0]
|
|
171
|
+
const state = getAdvisorState(session)
|
|
172
|
+
state.summary = { overallTask: 'x', activeSubtasks: [], keywords: [], todoVersion: 'aaa', turn: 1 }
|
|
173
|
+
state.lastSummaryTurn = 1
|
|
174
|
+
expect(invalidateOnTaskChange(state, 'bbb')).toBe(true)
|
|
175
|
+
expect(state.summary).toBeUndefined()
|
|
176
|
+
expect(state.lastSummaryTurn).toBe(-1)
|
|
177
|
+
expect(invalidateOnTaskChange(state, 'bbb')).toBe(false)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('advisor prompts (K7)', () => {
|
|
182
|
+
it('parses a well-formed summary answer', () => {
|
|
183
|
+
const parsed = parseAdvisorSummary(
|
|
184
|
+
'Sure! {"overallTask":"migrate gates","activeSubtasks":["port fresh gate"],"keywords":["gates","pruner"]}',
|
|
185
|
+
)
|
|
186
|
+
expect(parsed?.overallTask).toBe('migrate gates')
|
|
187
|
+
expect(parsed?.keywords).toEqual(['gates', 'pruner'])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('fails open on a malformed summary answer', () => {
|
|
191
|
+
expect(parseAdvisorSummary(undefined)).toBeUndefined()
|
|
192
|
+
expect(parseAdvisorSummary('')).toBeUndefined()
|
|
193
|
+
expect(parseAdvisorSummary('not json at all')).toBeUndefined()
|
|
194
|
+
expect(parseAdvisorSummary('{"overallTask":""}')).toBeUndefined()
|
|
195
|
+
expect(parseAdvisorSummary('{"overallTask":"x"}')).toBeUndefined()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('parses JSON-lines scoring answers and drops invalid rows', () => {
|
|
199
|
+
const parsed = parseAdvisorScores(
|
|
200
|
+
'{"seq":1,"score":0.9,"reason":"current task"}\n'
|
|
201
|
+
+ 'noise line {"seq":2,"score":1.5}\n'
|
|
202
|
+
+ '{"seq":99,"score":0.5}\n'
|
|
203
|
+
+ '{"seq":3,"score":0.1}\n',
|
|
204
|
+
new Set([1, 2, 3]),
|
|
205
|
+
)
|
|
206
|
+
expect(parsed?.size).toBe(2)
|
|
207
|
+
expect(parsed?.get(1)?.score).toBe(0.9)
|
|
208
|
+
expect(parsed?.get(3)?.score).toBe(0.1)
|
|
209
|
+
expect(parsed?.has(2)).toBe(false)
|
|
210
|
+
expect(parsed?.has(99)).toBe(false)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('fails open on empty or all-garbage scoring answers', () => {
|
|
214
|
+
expect(parseAdvisorScores(undefined, new Set([1]))).toBeUndefined()
|
|
215
|
+
expect(parseAdvisorScores('', new Set([1]))).toBeUndefined()
|
|
216
|
+
expect(parseAdvisorScores('garbage only', new Set([1]))).toBeUndefined()
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('builds a scoring prompt that keeps previews one-per-line', () => {
|
|
220
|
+
const prompt = buildAdvisorScoringUserPrompt('task text', ['sub'], [
|
|
221
|
+
{ seq: 7, preview: 'some\npreview' },
|
|
222
|
+
])
|
|
223
|
+
expect(prompt).toContain('task: task text')
|
|
224
|
+
expect(prompt).toContain('seq=7 | some preview')
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -170,18 +170,22 @@ describe('context-compression audit records', () => {
|
|
|
170
170
|
}).not.toThrow()
|
|
171
171
|
})
|
|
172
172
|
|
|
173
|
-
it('keeps
|
|
173
|
+
it('keeps reduction-advice records free of content, digests, and keys', () => {
|
|
174
174
|
const record: CompressionAuditRecord = {
|
|
175
175
|
schemaVersion: 1,
|
|
176
|
-
kind: '
|
|
177
|
-
sessionId: '
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
receiptStatus: 'applied',
|
|
176
|
+
kind: 'reduction-advice',
|
|
177
|
+
sessionId: 'advice-session',
|
|
178
|
+
profile: 'tokenpilot-inspired',
|
|
179
|
+
band: 'high-impact',
|
|
180
|
+
stage: 'fresh',
|
|
182
181
|
itemSeqs: [7, 9],
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
pricedCandidates: 2,
|
|
183
|
+
maxTokensBefore: 9_200,
|
|
184
|
+
tokensBefore: 12_400,
|
|
185
|
+
tokensAfter: 3_100,
|
|
186
|
+
recoveredTokens: 9_300,
|
|
187
|
+
penaltyTokens: 0,
|
|
188
|
+
paybackTurns: 0,
|
|
185
189
|
turnIndex: 12,
|
|
186
190
|
}
|
|
187
191
|
|
|
@@ -195,23 +199,33 @@ describe('context-compression audit records', () => {
|
|
|
195
199
|
expect(line).not.toContain('apiKey')
|
|
196
200
|
})
|
|
197
201
|
|
|
198
|
-
it('
|
|
202
|
+
it('describes an advised batch without any gate vocabulary', () => {
|
|
203
|
+
// The band can be the model's worst opinion — `not-worth-it` — and the
|
|
204
|
+
// record still only DESCRIBES a batch that landed: the retired review
|
|
205
|
+
// gate's decision vocabulary cannot be expressed any more.
|
|
199
206
|
const record: CompressionAuditRecord = {
|
|
200
207
|
schemaVersion: 1,
|
|
201
|
-
kind: '
|
|
202
|
-
sessionId: '
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
reasonCode: 'review_receipt_digest_invalid',
|
|
208
|
+
kind: 'reduction-advice',
|
|
209
|
+
sessionId: 'advice-session',
|
|
210
|
+
profile: 'tokenpilot-inspired',
|
|
211
|
+
band: 'not-worth-it',
|
|
212
|
+
stage: 'history',
|
|
207
213
|
itemSeqs: [3],
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
pricedCandidates: 1,
|
|
215
|
+
maxTokensBefore: 5_000,
|
|
216
|
+
tokensBefore: 5_000,
|
|
217
|
+
tokensAfter: 4_000,
|
|
218
|
+
recoveredTokens: 1_000,
|
|
219
|
+
penaltyTokens: 3_600,
|
|
220
|
+
paybackTurns: 36,
|
|
221
|
+
expectedSaving: 0,
|
|
210
222
|
}
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
) as CompressionAuditRecord
|
|
223
|
+
const line = formatCompressionAudit(record)
|
|
224
|
+
const parsed = JSON.parse(line.slice(COMPRESSION_AUDIT_PREFIX.length)) as CompressionAuditRecord
|
|
214
225
|
expect(parsed).toEqual(record)
|
|
226
|
+
for (const gone of ['proposalId', 'proposalKind', 'decision', 'receipt', '"event"', 'reasonCode']) {
|
|
227
|
+
expect(line).not.toContain(gone)
|
|
228
|
+
}
|
|
215
229
|
})
|
|
216
230
|
|
|
217
231
|
it('keeps advisor-outcome records free of prompts, keys, and content', () => {
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
|
|
3
|
-
import { CHARS_PER_TOKEN, charsForTokens, charsToTokens } from '../../src/runtime/config.ts'
|
|
4
|
-
|
|
5
|
-
describe('character basis conversion', () => {
|
|
6
|
-
it('derives zero tokens for non-positive or non-finite character counts', () => {
|
|
7
|
-
expect(charsToTokens(0)).toBe(0)
|
|
8
|
-
expect(charsToTokens(-1)).toBe(0)
|
|
9
|
-
expect(charsToTokens(NaN)).toBe(0)
|
|
10
|
-
expect(charsToTokens(Infinity)).toBe(0)
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it('derives the telemetry token figure from characters', () => {
|
|
14
|
-
expect(charsToTokens(1)).toBe(1)
|
|
15
|
-
expect(charsToTokens(4)).toBe(1)
|
|
16
|
-
expect(charsToTokens(5)).toBe(1)
|
|
17
|
-
expect(charsToTokens(8)).toBe(2)
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it('expresses token-named gates on the character basis', () => {
|
|
21
|
-
expect(charsForTokens(0)).toBe(0)
|
|
22
|
-
expect(charsForTokens(1000)).toBe(4000)
|
|
23
|
-
expect(CHARS_PER_TOKEN).toBe(4.0)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('stays finite at the integer upper bound (off/native gates must not misfire)', () => {
|
|
27
|
-
expect(Number.isFinite(charsForTokens(Number.MAX_SAFE_INTEGER))).toBe(true)
|
|
28
|
-
expect(charsForTokens(Number.MAX_SAFE_INTEGER)).toBeLessThan(Infinity)
|
|
29
|
-
})
|
|
30
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { CHARS_PER_TOKEN, charsForTokens, charsToTokens } from '../../src/runtime/config.ts'
|
|
4
|
+
|
|
5
|
+
describe('character basis conversion', () => {
|
|
6
|
+
it('derives zero tokens for non-positive or non-finite character counts', () => {
|
|
7
|
+
expect(charsToTokens(0)).toBe(0)
|
|
8
|
+
expect(charsToTokens(-1)).toBe(0)
|
|
9
|
+
expect(charsToTokens(NaN)).toBe(0)
|
|
10
|
+
expect(charsToTokens(Infinity)).toBe(0)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('derives the telemetry token figure from characters', () => {
|
|
14
|
+
expect(charsToTokens(1)).toBe(1)
|
|
15
|
+
expect(charsToTokens(4)).toBe(1)
|
|
16
|
+
expect(charsToTokens(5)).toBe(1)
|
|
17
|
+
expect(charsToTokens(8)).toBe(2)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('expresses token-named gates on the character basis', () => {
|
|
21
|
+
expect(charsForTokens(0)).toBe(0)
|
|
22
|
+
expect(charsForTokens(1000)).toBe(4000)
|
|
23
|
+
expect(CHARS_PER_TOKEN).toBe(4.0)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('stays finite at the integer upper bound (off/native gates must not misfire)', () => {
|
|
27
|
+
expect(Number.isFinite(charsForTokens(Number.MAX_SAFE_INTEGER))).toBe(true)
|
|
28
|
+
expect(charsForTokens(Number.MAX_SAFE_INTEGER)).toBeLessThan(Infinity)
|
|
29
|
+
})
|
|
30
|
+
})
|