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,281 +1,281 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* toolclass-corpus-replay.mjs — 真实日志离线复算(task_5 / G3)。
|
|
4
|
-
*
|
|
5
|
-
* 对 `~/.dsh/sessions/**\/session*.jsonl.zstd` 多帧 zstd 会话日志:
|
|
6
|
-
* 扫魔数 28 B5 2F FD 逐帧解压(帧去重:并发写入会落重复帧)
|
|
7
|
-
* → 配对 tool/call ↔ tool/result(callId)
|
|
8
|
-
* → 直调 reduceFreshToolResult(内部无门槛,门槛在 planFresh;本脚本自带
|
|
9
|
-
* budgetChars 模拟,不测端到端 —— 必读①:验收必须在 reducer 层)
|
|
10
|
-
* → 输出 ToolClass 分发矩阵 / reducer 命中率 / 压缩率 / 误路由对照 /
|
|
11
|
-
* freshTriggerTokens 敏感性表。
|
|
12
|
-
*
|
|
13
|
-
* 用法:
|
|
14
|
-
* node scripts/toolclass-corpus-replay.mjs [--dir <sessionsDir>] [--limit <N>]
|
|
15
|
-
* [--session <substring>] [--min-chars <N>] [--budget-ratio <f>] [--out <report.md>]
|
|
16
|
-
*
|
|
17
|
-
* 依赖:先 `pnpm build` 生成 packages/selector/lib/pruner.js。
|
|
18
|
-
* 样本偏差声明(必读⑨/RK-5):--limit 取的是体积最大的会话(偏长会话),
|
|
19
|
-
* 结论不得外推到全体会话;报告须标注会话数/样本数/时间范围。
|
|
20
|
-
* ⚠️ 口径标注:自 7a1972a(字符基准闸门)起,运行时决策按字符(characters)执行,
|
|
21
|
-
* 本脚本输出的压缩率/预算均为 reducer 层字符口径;tokens 字段仅为遥测派生(chars/4.0),
|
|
22
|
-
* 不得当作运行时决策依据。
|
|
23
|
-
* ⚠️ --min-chars 默认 14000 只是**本脚本的样本过滤下限**,与运行时
|
|
24
|
-
* `READ_TOC_MIN_CHARS`(reducers.ts)数值撞值但**毫无派生关系**:运行时的
|
|
25
|
-
* fresh 门槛是 freshTriggerTokens(8192 tok ≈ 29.5k 字符),恒高于 14k 字符,
|
|
26
|
-
* 本脚本过滤值从不参与运行时行为(findings §16)。
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
30
|
-
import { homedir } from 'node:os'
|
|
31
|
-
import { join, relative } from 'node:path'
|
|
32
|
-
import { zstdDecompressSync } from 'node:zlib'
|
|
33
|
-
|
|
34
|
-
const REPO_ROOT = join(import.meta.dirname, '..')
|
|
35
|
-
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
36
|
-
|
|
37
|
-
function parseArgs(argv) {
|
|
38
|
-
const args = {
|
|
39
|
-
dir: join(homedir(), '.dsh', 'sessions'),
|
|
40
|
-
limit: 20,
|
|
41
|
-
session: '',
|
|
42
|
-
minChars: 14_000,
|
|
43
|
-
budgetRatio: 0.75,
|
|
44
|
-
out: '',
|
|
45
|
-
}
|
|
46
|
-
for (let index = 2; index < argv.length; index++) {
|
|
47
|
-
const key = argv[index]
|
|
48
|
-
const value = argv[index + 1]
|
|
49
|
-
if (key === '--dir') { args.dir = value; index++ }
|
|
50
|
-
else if (key === '--limit') { args.limit = Number(value); index++ }
|
|
51
|
-
else if (key === '--session') { args.session = value; index++ }
|
|
52
|
-
else if (key === '--min-chars') { args.minChars = Number(value); index++ }
|
|
53
|
-
else if (key === '--budget-ratio') { args.budgetRatio = Number(value); index++ }
|
|
54
|
-
else if (key === '--out') { args.out = value; index++ }
|
|
55
|
-
else { console.error(`unknown arg ${key}`); process.exit(2) }
|
|
56
|
-
}
|
|
57
|
-
return args
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 多帧 zstd:扫魔数逐帧解压;对解出的 JSONL 行全局去重(等价于帧去重,
|
|
61
|
-
* 且对"重复帧但行交错"的场景更稳)。 */
|
|
62
|
-
function decodeMultiFrameZstd(buffer) {
|
|
63
|
-
const offsets = []
|
|
64
|
-
let position = 0
|
|
65
|
-
for (;;) {
|
|
66
|
-
const index = buffer.indexOf(ZSTD_MAGIC, position)
|
|
67
|
-
if (index < 0) break
|
|
68
|
-
offsets.push(index)
|
|
69
|
-
position = index + 1
|
|
70
|
-
}
|
|
71
|
-
const chunks = []
|
|
72
|
-
let failedFrames = 0
|
|
73
|
-
for (let index = 0; index < offsets.length; index++) {
|
|
74
|
-
const chunk = buffer.subarray(offsets[index], index + 1 < offsets.length ? offsets[index + 1] : buffer.length)
|
|
75
|
-
try {
|
|
76
|
-
chunks.push(zstdDecompressSync(chunk))
|
|
77
|
-
} catch {
|
|
78
|
-
failedFrames += 1
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const seen = new Set()
|
|
82
|
-
const lines = []
|
|
83
|
-
for (const chunk of chunks) {
|
|
84
|
-
for (const line of chunk.toString('utf8').split('\n')) {
|
|
85
|
-
if (line.trim() === '' || seen.has(line)) continue
|
|
86
|
-
seen.add(line)
|
|
87
|
-
lines.push(line)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return { lines, frames: offsets.length, failedFrames }
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function listSessionFiles(dir, sessionFilter) {
|
|
94
|
-
const files = []
|
|
95
|
-
const walk = current => {
|
|
96
|
-
let entries
|
|
97
|
-
try { entries = readdirSync(current, { withFileTypes: true }) } catch { return }
|
|
98
|
-
for (const entry of entries) {
|
|
99
|
-
const path = join(current, entry.name)
|
|
100
|
-
if (entry.isDirectory()) walk(path)
|
|
101
|
-
else if (entry.name.startsWith('session') && entry.name.endsWith('.jsonl.zstd')) files.push(path)
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
walk(dir)
|
|
105
|
-
return files.filter(path => sessionFilter === '' || path.includes(sessionFilter))
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** tool/call ↔ tool/result 配对;返回 {toolName, arguments, text, isError, seq, time}。 */
|
|
109
|
-
function extractToolResults(lines) {
|
|
110
|
-
const calls = new Map()
|
|
111
|
-
const results = []
|
|
112
|
-
for (const line of lines) {
|
|
113
|
-
let event
|
|
114
|
-
try { event = JSON.parse(line) } catch { continue }
|
|
115
|
-
if (event?.type === 'tool/call') {
|
|
116
|
-
const callId = event.data?.callId
|
|
117
|
-
if (typeof callId === 'string') {
|
|
118
|
-
calls.set(callId, { name: event.data?.name ?? '', arguments: event.data?.arguments ?? '{}' })
|
|
119
|
-
}
|
|
120
|
-
} else if (event?.type === 'tool/result') {
|
|
121
|
-
const message = event.data?.message
|
|
122
|
-
const callId = message?.source?.callId
|
|
123
|
-
const block = message?.content?.[0]
|
|
124
|
-
const text = block?.content?.find(part => part?.type === 'text')?.text
|
|
125
|
-
if (typeof callId !== 'string' || typeof text !== 'string') continue
|
|
126
|
-
results.push({
|
|
127
|
-
callId,
|
|
128
|
-
toolName: calls.get(callId)?.name ?? '(unknown)',
|
|
129
|
-
argumentsText: calls.get(callId)?.arguments ?? '{}',
|
|
130
|
-
text,
|
|
131
|
-
isError: block?.isError === true,
|
|
132
|
-
seq: event.seq,
|
|
133
|
-
time: event.time,
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return results
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** v1 时代的旧分类(子串正则),仅用于误路由对照,不参与现行为。 */
|
|
141
|
-
function legacyIsSearchTool(name, command) {
|
|
142
|
-
return /(?:grep|search|glob|find|ripgrep|rg)/.test(name)
|
|
143
|
-
|| /(?:^|\s)(?:rg|grep|find|fd)\s/.test(command)
|
|
144
|
-
}
|
|
145
|
-
function legacyCommandKeys(argumentsText) {
|
|
146
|
-
try {
|
|
147
|
-
const parsed = JSON.parse(argumentsText)
|
|
148
|
-
if (typeof parsed !== 'object' || parsed === null) return ''
|
|
149
|
-
for (const key of ['command', 'cmd', 'script', 'input']) {
|
|
150
|
-
if (typeof parsed[key] === 'string') return parsed[key]
|
|
151
|
-
}
|
|
152
|
-
} catch { /* ignore */ }
|
|
153
|
-
return ''
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function main() {
|
|
157
|
-
const args = parseArgs(process.argv)
|
|
158
|
-
const lib = await import(new URL(`file://${join(REPO_ROOT, 'packages/selector/lib/pruner.js').replace(/\\/g, '/')}`).href)
|
|
159
|
-
const { reduceFreshToolResult, resolvePolicy, COMPRESSION_PROFILES } = lib
|
|
160
|
-
if (typeof reduceFreshToolResult !== 'function') throw new Error('built lib missing reduceFreshToolResult — run `pnpm build` first')
|
|
161
|
-
|
|
162
|
-
const policy = resolvePolicy({}, 'balanced')
|
|
163
|
-
console.log(`[policy] balanced: freshTriggerTokens = ${policy.freshTriggerTokens} | freshTargetTokens = ${policy.freshTargetTokens} | freshEnabled = ${policy.freshEnabled}`)
|
|
164
|
-
|
|
165
|
-
const files = listSessionFiles(args.dir, args.session)
|
|
166
|
-
.map(path => ({ path, size: statSync(path).size }))
|
|
167
|
-
.sort((a, b) => b.size - a.size)
|
|
168
|
-
.slice(0, args.limit)
|
|
169
|
-
console.log(`[corpus] ${files.length} session file(s) (limit=${args.limit}, min-chars=${args.minChars}, dir=${args.dir})`)
|
|
170
|
-
console.log(`[note] --min-chars is a sample filter for this script only; it is NOT the runtime READ_TOC_MIN_CHARS and does not gate runtime behavior`)
|
|
171
|
-
|
|
172
|
-
const dispatch = new Map() // toolName → toolClass → { reducer → count }
|
|
173
|
-
const reducerHits = new Map()
|
|
174
|
-
const classTotals = new Map()
|
|
175
|
-
const misroutes = []
|
|
176
|
-
let totalIn = 0
|
|
177
|
-
let totalOut = 0
|
|
178
|
-
let reduced = 0
|
|
179
|
-
let failOpen = 0
|
|
180
|
-
let sessions = 0
|
|
181
|
-
let minTime = Number.POSITIVE_INFINITY
|
|
182
|
-
let maxTime = Number.NEGATIVE_INFINITY
|
|
183
|
-
const readSizes = []
|
|
184
|
-
|
|
185
|
-
for (const file of files) {
|
|
186
|
-
sessions += 1
|
|
187
|
-
const raw = readFileSync(file.path)
|
|
188
|
-
const { lines, frames, failedFrames } = decodeMultiFrameZstd(raw)
|
|
189
|
-
if (failedFrames > 0) console.warn(` [warn] ${relative(args.dir, file.path)}: ${failedFrames}/${frames} frame(s) failed to decode`)
|
|
190
|
-
for (const result of extractToolResults(lines)) {
|
|
191
|
-
if (result.text.length < args.minChars) continue
|
|
192
|
-
minTime = Math.min(minTime, result.time)
|
|
193
|
-
maxTime = Math.max(maxTime, result.time)
|
|
194
|
-
const budgetChars = Math.floor(result.text.length * args.budgetRatio)
|
|
195
|
-
const output = reduceFreshToolResult({
|
|
196
|
-
toolName: result.toolName,
|
|
197
|
-
argumentsText: result.argumentsText,
|
|
198
|
-
text: result.text,
|
|
199
|
-
budgetChars,
|
|
200
|
-
sourceRef: `session://replay/${String(result.seq)}`,
|
|
201
|
-
isError: result.isError,
|
|
202
|
-
codeSkeleton: true,
|
|
203
|
-
})
|
|
204
|
-
const command = legacyCommandKeys(result.argumentsText)
|
|
205
|
-
const lowered = result.toolName.toLowerCase()
|
|
206
|
-
const legacySearch = legacyIsSearchTool(lowered, command)
|
|
207
|
-
const nowSearchRoute = output?.reducer === 'search-by-file' || output?.reducer === 'search-salience'
|
|
208
|
-
if (legacySearch && !nowSearchRoute && misroutes.length < 20) {
|
|
209
|
-
misroutes.push({
|
|
210
|
-
tool: result.toolName,
|
|
211
|
-
chars: result.text.length,
|
|
212
|
-
head: Array.from(result.text.slice(0, 80).replace(/\n/g, '\\n')),
|
|
213
|
-
now: output?.reducer ?? 'fail-open',
|
|
214
|
-
})
|
|
215
|
-
}
|
|
216
|
-
void legacySearch
|
|
217
|
-
// 分发矩阵:工具名 → 现派发 reducer → 计数
|
|
218
|
-
const byTool = dispatch.get(result.toolName) ?? {}
|
|
219
|
-
const key = output === null ? 'fail-open' : output.reducer
|
|
220
|
-
byTool[key] = (byTool[key] ?? 0) + 1
|
|
221
|
-
dispatch.set(result.toolName, byTool)
|
|
222
|
-
reducerHits.set(key, (reducerHits.get(key) ?? 0) + 1)
|
|
223
|
-
classTotals.set(result.toolName, (classTotals.get(result.toolName) ?? 0) + 1)
|
|
224
|
-
totalIn += result.text.length
|
|
225
|
-
if (output !== null) {
|
|
226
|
-
reduced += 1
|
|
227
|
-
totalOut += output.text.length
|
|
228
|
-
} else {
|
|
229
|
-
failOpen += 1
|
|
230
|
-
}
|
|
231
|
-
if (result.toolName.toLowerCase().match(/read|cat|view/)) readSizes.push(result.text.length)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const lines = []
|
|
236
|
-
lines.push(`# toolclass corpus replay — ${new Date().toISOString()}`)
|
|
237
|
-
lines.push(`- sessions: ${sessions} (top by size; BIAS: favors long sessions — do not extrapolate)`)
|
|
238
|
-
lines.push(`- sample window (event time): ${Number.isFinite(minTime) ? new Date(minTime).toISOString() : 'n/a'} … ${Number.isFinite(maxTime) ? new Date(maxTime).toISOString() : 'n/a'}`)
|
|
239
|
-
lines.push(`- samples ≥ ${args.minChars} chars: ${reduced + failOpen} (reduced ${reduced}, fail-open ${failOpen})`)
|
|
240
|
-
lines.push(`- verify pass rate (non-null): ${reduced + failOpen === 0 ? 'n/a' : `${(reduced / (reduced + failOpen) * 100).toFixed(1)}%`}`)
|
|
241
|
-
lines.push(`- compression: ${totalIn} → ${totalOut} chars (${totalIn === 0 ? 'n/a' : `${(totalOut / totalIn * 100).toFixed(1)}%`})`)
|
|
242
|
-
lines.push('')
|
|
243
|
-
lines.push('## reducer hit rate')
|
|
244
|
-
for (const [key, count] of [...reducerHits.entries()].sort((a, b) => b[1] - a[1])) {
|
|
245
|
-
lines.push(`- ${key}: ${count}`)
|
|
246
|
-
}
|
|
247
|
-
lines.push('')
|
|
248
|
-
lines.push('## dispatch matrix (toolName → reducer → count)')
|
|
249
|
-
for (const [toolName, byReducer] of [...dispatch.entries()]
|
|
250
|
-
.sort((a, b) => Object.values(b[1]).reduce((x, y) => x + y, 0) - Object.values(a[1]).reduce((x, y) => x + y, 0))) {
|
|
251
|
-
lines.push(`- ${toolName}: ${JSON.stringify(byReducer)}`)
|
|
252
|
-
}
|
|
253
|
-
lines.push('')
|
|
254
|
-
lines.push('## fixed misroutes (legacy substring → search, now dispatched elsewhere)')
|
|
255
|
-
for (const sample of misroutes) {
|
|
256
|
-
lines.push(`- ${sample.tool} (${sample.chars} chars) → ${sample.now} | head: ${sample.head.join('')}`)
|
|
257
|
-
}
|
|
258
|
-
if (misroutes.length === 0) lines.push('- (none in this sample)')
|
|
259
|
-
lines.push('')
|
|
260
|
-
lines.push('## freshTriggerTokens sensitivity (read-class results, chars thresholds)')
|
|
261
|
-
for (const trigger of [8_192, 6_144, 4_096, 2_048]) {
|
|
262
|
-
const t36 = trigger * 3.6
|
|
263
|
-
const t40 = trigger * 4.0
|
|
264
|
-
const above36 = readSizes.filter(size => size > t36).length
|
|
265
|
-
const above40 = readSizes.filter(size => size > t40).length
|
|
266
|
-
lines.push(`- trigger ${trigger}: read samples > ${t40.toFixed(0)} chars (4.0 c/t): ${above40}/${readSizes.length}; > ${t36.toFixed(0)} (3.6 c/t): ${above36}/${readSizes.length}`)
|
|
267
|
-
}
|
|
268
|
-
lines.push('')
|
|
269
|
-
lines.push(`> profiles available: ${COMPRESSION_PROFILES.join(', ')}`)
|
|
270
|
-
const report = lines.join('\n')
|
|
271
|
-
console.log('\n' + report)
|
|
272
|
-
if (args.out !== '') {
|
|
273
|
-
writeFileSync(args.out, report, 'utf8')
|
|
274
|
-
console.log(`[written] ${args.out}`)
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
main().catch(error => {
|
|
279
|
-
console.error(error)
|
|
280
|
-
process.exit(1)
|
|
281
|
-
})
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* toolclass-corpus-replay.mjs — 真实日志离线复算(task_5 / G3)。
|
|
4
|
+
*
|
|
5
|
+
* 对 `~/.dsh/sessions/**\/session*.jsonl.zstd` 多帧 zstd 会话日志:
|
|
6
|
+
* 扫魔数 28 B5 2F FD 逐帧解压(帧去重:并发写入会落重复帧)
|
|
7
|
+
* → 配对 tool/call ↔ tool/result(callId)
|
|
8
|
+
* → 直调 reduceFreshToolResult(内部无门槛,门槛在 planFresh;本脚本自带
|
|
9
|
+
* budgetChars 模拟,不测端到端 —— 必读①:验收必须在 reducer 层)
|
|
10
|
+
* → 输出 ToolClass 分发矩阵 / reducer 命中率 / 压缩率 / 误路由对照 /
|
|
11
|
+
* freshTriggerTokens 敏感性表。
|
|
12
|
+
*
|
|
13
|
+
* 用法:
|
|
14
|
+
* node scripts/toolclass-corpus-replay.mjs [--dir <sessionsDir>] [--limit <N>]
|
|
15
|
+
* [--session <substring>] [--min-chars <N>] [--budget-ratio <f>] [--out <report.md>]
|
|
16
|
+
*
|
|
17
|
+
* 依赖:先 `pnpm build` 生成 packages/selector/lib/pruner.js。
|
|
18
|
+
* 样本偏差声明(必读⑨/RK-5):--limit 取的是体积最大的会话(偏长会话),
|
|
19
|
+
* 结论不得外推到全体会话;报告须标注会话数/样本数/时间范围。
|
|
20
|
+
* ⚠️ 口径标注:自 7a1972a(字符基准闸门)起,运行时决策按字符(characters)执行,
|
|
21
|
+
* 本脚本输出的压缩率/预算均为 reducer 层字符口径;tokens 字段仅为遥测派生(chars/4.0),
|
|
22
|
+
* 不得当作运行时决策依据。
|
|
23
|
+
* ⚠️ --min-chars 默认 14000 只是**本脚本的样本过滤下限**,与运行时
|
|
24
|
+
* `READ_TOC_MIN_CHARS`(reducers.ts)数值撞值但**毫无派生关系**:运行时的
|
|
25
|
+
* fresh 门槛是 freshTriggerTokens(8192 tok ≈ 29.5k 字符),恒高于 14k 字符,
|
|
26
|
+
* 本脚本过滤值从不参与运行时行为(findings §16)。
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
30
|
+
import { homedir } from 'node:os'
|
|
31
|
+
import { join, relative } from 'node:path'
|
|
32
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
33
|
+
|
|
34
|
+
const REPO_ROOT = join(import.meta.dirname, '..')
|
|
35
|
+
const ZSTD_MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const args = {
|
|
39
|
+
dir: join(homedir(), '.dsh', 'sessions'),
|
|
40
|
+
limit: 20,
|
|
41
|
+
session: '',
|
|
42
|
+
minChars: 14_000,
|
|
43
|
+
budgetRatio: 0.75,
|
|
44
|
+
out: '',
|
|
45
|
+
}
|
|
46
|
+
for (let index = 2; index < argv.length; index++) {
|
|
47
|
+
const key = argv[index]
|
|
48
|
+
const value = argv[index + 1]
|
|
49
|
+
if (key === '--dir') { args.dir = value; index++ }
|
|
50
|
+
else if (key === '--limit') { args.limit = Number(value); index++ }
|
|
51
|
+
else if (key === '--session') { args.session = value; index++ }
|
|
52
|
+
else if (key === '--min-chars') { args.minChars = Number(value); index++ }
|
|
53
|
+
else if (key === '--budget-ratio') { args.budgetRatio = Number(value); index++ }
|
|
54
|
+
else if (key === '--out') { args.out = value; index++ }
|
|
55
|
+
else { console.error(`unknown arg ${key}`); process.exit(2) }
|
|
56
|
+
}
|
|
57
|
+
return args
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 多帧 zstd:扫魔数逐帧解压;对解出的 JSONL 行全局去重(等价于帧去重,
|
|
61
|
+
* 且对"重复帧但行交错"的场景更稳)。 */
|
|
62
|
+
function decodeMultiFrameZstd(buffer) {
|
|
63
|
+
const offsets = []
|
|
64
|
+
let position = 0
|
|
65
|
+
for (;;) {
|
|
66
|
+
const index = buffer.indexOf(ZSTD_MAGIC, position)
|
|
67
|
+
if (index < 0) break
|
|
68
|
+
offsets.push(index)
|
|
69
|
+
position = index + 1
|
|
70
|
+
}
|
|
71
|
+
const chunks = []
|
|
72
|
+
let failedFrames = 0
|
|
73
|
+
for (let index = 0; index < offsets.length; index++) {
|
|
74
|
+
const chunk = buffer.subarray(offsets[index], index + 1 < offsets.length ? offsets[index + 1] : buffer.length)
|
|
75
|
+
try {
|
|
76
|
+
chunks.push(zstdDecompressSync(chunk))
|
|
77
|
+
} catch {
|
|
78
|
+
failedFrames += 1
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const seen = new Set()
|
|
82
|
+
const lines = []
|
|
83
|
+
for (const chunk of chunks) {
|
|
84
|
+
for (const line of chunk.toString('utf8').split('\n')) {
|
|
85
|
+
if (line.trim() === '' || seen.has(line)) continue
|
|
86
|
+
seen.add(line)
|
|
87
|
+
lines.push(line)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { lines, frames: offsets.length, failedFrames }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listSessionFiles(dir, sessionFilter) {
|
|
94
|
+
const files = []
|
|
95
|
+
const walk = current => {
|
|
96
|
+
let entries
|
|
97
|
+
try { entries = readdirSync(current, { withFileTypes: true }) } catch { return }
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const path = join(current, entry.name)
|
|
100
|
+
if (entry.isDirectory()) walk(path)
|
|
101
|
+
else if (entry.name.startsWith('session') && entry.name.endsWith('.jsonl.zstd')) files.push(path)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
walk(dir)
|
|
105
|
+
return files.filter(path => sessionFilter === '' || path.includes(sessionFilter))
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** tool/call ↔ tool/result 配对;返回 {toolName, arguments, text, isError, seq, time}。 */
|
|
109
|
+
function extractToolResults(lines) {
|
|
110
|
+
const calls = new Map()
|
|
111
|
+
const results = []
|
|
112
|
+
for (const line of lines) {
|
|
113
|
+
let event
|
|
114
|
+
try { event = JSON.parse(line) } catch { continue }
|
|
115
|
+
if (event?.type === 'tool/call') {
|
|
116
|
+
const callId = event.data?.callId
|
|
117
|
+
if (typeof callId === 'string') {
|
|
118
|
+
calls.set(callId, { name: event.data?.name ?? '', arguments: event.data?.arguments ?? '{}' })
|
|
119
|
+
}
|
|
120
|
+
} else if (event?.type === 'tool/result') {
|
|
121
|
+
const message = event.data?.message
|
|
122
|
+
const callId = message?.source?.callId
|
|
123
|
+
const block = message?.content?.[0]
|
|
124
|
+
const text = block?.content?.find(part => part?.type === 'text')?.text
|
|
125
|
+
if (typeof callId !== 'string' || typeof text !== 'string') continue
|
|
126
|
+
results.push({
|
|
127
|
+
callId,
|
|
128
|
+
toolName: calls.get(callId)?.name ?? '(unknown)',
|
|
129
|
+
argumentsText: calls.get(callId)?.arguments ?? '{}',
|
|
130
|
+
text,
|
|
131
|
+
isError: block?.isError === true,
|
|
132
|
+
seq: event.seq,
|
|
133
|
+
time: event.time,
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return results
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** v1 时代的旧分类(子串正则),仅用于误路由对照,不参与现行为。 */
|
|
141
|
+
function legacyIsSearchTool(name, command) {
|
|
142
|
+
return /(?:grep|search|glob|find|ripgrep|rg)/.test(name)
|
|
143
|
+
|| /(?:^|\s)(?:rg|grep|find|fd)\s/.test(command)
|
|
144
|
+
}
|
|
145
|
+
function legacyCommandKeys(argumentsText) {
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(argumentsText)
|
|
148
|
+
if (typeof parsed !== 'object' || parsed === null) return ''
|
|
149
|
+
for (const key of ['command', 'cmd', 'script', 'input']) {
|
|
150
|
+
if (typeof parsed[key] === 'string') return parsed[key]
|
|
151
|
+
}
|
|
152
|
+
} catch { /* ignore */ }
|
|
153
|
+
return ''
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function main() {
|
|
157
|
+
const args = parseArgs(process.argv)
|
|
158
|
+
const lib = await import(new URL(`file://${join(REPO_ROOT, 'packages/selector/lib/pruner.js').replace(/\\/g, '/')}`).href)
|
|
159
|
+
const { reduceFreshToolResult, resolvePolicy, COMPRESSION_PROFILES } = lib
|
|
160
|
+
if (typeof reduceFreshToolResult !== 'function') throw new Error('built lib missing reduceFreshToolResult — run `pnpm build` first')
|
|
161
|
+
|
|
162
|
+
const policy = resolvePolicy({}, 'balanced')
|
|
163
|
+
console.log(`[policy] balanced: freshTriggerTokens = ${policy.freshTriggerTokens} | freshTargetTokens = ${policy.freshTargetTokens} | freshEnabled = ${policy.freshEnabled}`)
|
|
164
|
+
|
|
165
|
+
const files = listSessionFiles(args.dir, args.session)
|
|
166
|
+
.map(path => ({ path, size: statSync(path).size }))
|
|
167
|
+
.sort((a, b) => b.size - a.size)
|
|
168
|
+
.slice(0, args.limit)
|
|
169
|
+
console.log(`[corpus] ${files.length} session file(s) (limit=${args.limit}, min-chars=${args.minChars}, dir=${args.dir})`)
|
|
170
|
+
console.log(`[note] --min-chars is a sample filter for this script only; it is NOT the runtime READ_TOC_MIN_CHARS and does not gate runtime behavior`)
|
|
171
|
+
|
|
172
|
+
const dispatch = new Map() // toolName → toolClass → { reducer → count }
|
|
173
|
+
const reducerHits = new Map()
|
|
174
|
+
const classTotals = new Map()
|
|
175
|
+
const misroutes = []
|
|
176
|
+
let totalIn = 0
|
|
177
|
+
let totalOut = 0
|
|
178
|
+
let reduced = 0
|
|
179
|
+
let failOpen = 0
|
|
180
|
+
let sessions = 0
|
|
181
|
+
let minTime = Number.POSITIVE_INFINITY
|
|
182
|
+
let maxTime = Number.NEGATIVE_INFINITY
|
|
183
|
+
const readSizes = []
|
|
184
|
+
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
sessions += 1
|
|
187
|
+
const raw = readFileSync(file.path)
|
|
188
|
+
const { lines, frames, failedFrames } = decodeMultiFrameZstd(raw)
|
|
189
|
+
if (failedFrames > 0) console.warn(` [warn] ${relative(args.dir, file.path)}: ${failedFrames}/${frames} frame(s) failed to decode`)
|
|
190
|
+
for (const result of extractToolResults(lines)) {
|
|
191
|
+
if (result.text.length < args.minChars) continue
|
|
192
|
+
minTime = Math.min(minTime, result.time)
|
|
193
|
+
maxTime = Math.max(maxTime, result.time)
|
|
194
|
+
const budgetChars = Math.floor(result.text.length * args.budgetRatio)
|
|
195
|
+
const output = reduceFreshToolResult({
|
|
196
|
+
toolName: result.toolName,
|
|
197
|
+
argumentsText: result.argumentsText,
|
|
198
|
+
text: result.text,
|
|
199
|
+
budgetChars,
|
|
200
|
+
sourceRef: `session://replay/${String(result.seq)}`,
|
|
201
|
+
isError: result.isError,
|
|
202
|
+
codeSkeleton: true,
|
|
203
|
+
})
|
|
204
|
+
const command = legacyCommandKeys(result.argumentsText)
|
|
205
|
+
const lowered = result.toolName.toLowerCase()
|
|
206
|
+
const legacySearch = legacyIsSearchTool(lowered, command)
|
|
207
|
+
const nowSearchRoute = output?.reducer === 'search-by-file' || output?.reducer === 'search-salience'
|
|
208
|
+
if (legacySearch && !nowSearchRoute && misroutes.length < 20) {
|
|
209
|
+
misroutes.push({
|
|
210
|
+
tool: result.toolName,
|
|
211
|
+
chars: result.text.length,
|
|
212
|
+
head: Array.from(result.text.slice(0, 80).replace(/\n/g, '\\n')),
|
|
213
|
+
now: output?.reducer ?? 'fail-open',
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
void legacySearch
|
|
217
|
+
// 分发矩阵:工具名 → 现派发 reducer → 计数
|
|
218
|
+
const byTool = dispatch.get(result.toolName) ?? {}
|
|
219
|
+
const key = output === null ? 'fail-open' : output.reducer
|
|
220
|
+
byTool[key] = (byTool[key] ?? 0) + 1
|
|
221
|
+
dispatch.set(result.toolName, byTool)
|
|
222
|
+
reducerHits.set(key, (reducerHits.get(key) ?? 0) + 1)
|
|
223
|
+
classTotals.set(result.toolName, (classTotals.get(result.toolName) ?? 0) + 1)
|
|
224
|
+
totalIn += result.text.length
|
|
225
|
+
if (output !== null) {
|
|
226
|
+
reduced += 1
|
|
227
|
+
totalOut += output.text.length
|
|
228
|
+
} else {
|
|
229
|
+
failOpen += 1
|
|
230
|
+
}
|
|
231
|
+
if (result.toolName.toLowerCase().match(/read|cat|view/)) readSizes.push(result.text.length)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const lines = []
|
|
236
|
+
lines.push(`# toolclass corpus replay — ${new Date().toISOString()}`)
|
|
237
|
+
lines.push(`- sessions: ${sessions} (top by size; BIAS: favors long sessions — do not extrapolate)`)
|
|
238
|
+
lines.push(`- sample window (event time): ${Number.isFinite(minTime) ? new Date(minTime).toISOString() : 'n/a'} … ${Number.isFinite(maxTime) ? new Date(maxTime).toISOString() : 'n/a'}`)
|
|
239
|
+
lines.push(`- samples ≥ ${args.minChars} chars: ${reduced + failOpen} (reduced ${reduced}, fail-open ${failOpen})`)
|
|
240
|
+
lines.push(`- verify pass rate (non-null): ${reduced + failOpen === 0 ? 'n/a' : `${(reduced / (reduced + failOpen) * 100).toFixed(1)}%`}`)
|
|
241
|
+
lines.push(`- compression: ${totalIn} → ${totalOut} chars (${totalIn === 0 ? 'n/a' : `${(totalOut / totalIn * 100).toFixed(1)}%`})`)
|
|
242
|
+
lines.push('')
|
|
243
|
+
lines.push('## reducer hit rate')
|
|
244
|
+
for (const [key, count] of [...reducerHits.entries()].sort((a, b) => b[1] - a[1])) {
|
|
245
|
+
lines.push(`- ${key}: ${count}`)
|
|
246
|
+
}
|
|
247
|
+
lines.push('')
|
|
248
|
+
lines.push('## dispatch matrix (toolName → reducer → count)')
|
|
249
|
+
for (const [toolName, byReducer] of [...dispatch.entries()]
|
|
250
|
+
.sort((a, b) => Object.values(b[1]).reduce((x, y) => x + y, 0) - Object.values(a[1]).reduce((x, y) => x + y, 0))) {
|
|
251
|
+
lines.push(`- ${toolName}: ${JSON.stringify(byReducer)}`)
|
|
252
|
+
}
|
|
253
|
+
lines.push('')
|
|
254
|
+
lines.push('## fixed misroutes (legacy substring → search, now dispatched elsewhere)')
|
|
255
|
+
for (const sample of misroutes) {
|
|
256
|
+
lines.push(`- ${sample.tool} (${sample.chars} chars) → ${sample.now} | head: ${sample.head.join('')}`)
|
|
257
|
+
}
|
|
258
|
+
if (misroutes.length === 0) lines.push('- (none in this sample)')
|
|
259
|
+
lines.push('')
|
|
260
|
+
lines.push('## freshTriggerTokens sensitivity (read-class results, chars thresholds)')
|
|
261
|
+
for (const trigger of [8_192, 6_144, 4_096, 2_048]) {
|
|
262
|
+
const t36 = trigger * 3.6
|
|
263
|
+
const t40 = trigger * 4.0
|
|
264
|
+
const above36 = readSizes.filter(size => size > t36).length
|
|
265
|
+
const above40 = readSizes.filter(size => size > t40).length
|
|
266
|
+
lines.push(`- trigger ${trigger}: read samples > ${t40.toFixed(0)} chars (4.0 c/t): ${above40}/${readSizes.length}; > ${t36.toFixed(0)} (3.6 c/t): ${above36}/${readSizes.length}`)
|
|
267
|
+
}
|
|
268
|
+
lines.push('')
|
|
269
|
+
lines.push(`> profiles available: ${COMPRESSION_PROFILES.join(', ')}`)
|
|
270
|
+
const report = lines.join('\n')
|
|
271
|
+
console.log('\n' + report)
|
|
272
|
+
if (args.out !== '') {
|
|
273
|
+
writeFileSync(args.out, report, 'utf8')
|
|
274
|
+
console.log(`[written] ${args.out}`)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
main().catch(error => {
|
|
279
|
+
console.error(error)
|
|
280
|
+
process.exit(1)
|
|
281
|
+
})
|