rcs-context 0.0.0-stage → 0.1.40
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/LICENSE.txt +21 -0
- package/README.md +204 -2
- package/condense/chunk.mjs +258 -0
- package/condense/ivr.mjs +104 -0
- package/contextinjector-webui/README.md +43 -0
- package/contextinjector-webui/lib/client.js +3034 -0
- package/contextinjector-webui/lib/index.mjs +1565 -0
- package/contextinjector-webui/lib/trace-read.mjs +180 -0
- package/contextinjector-webui/package.json +21 -0
- package/cordis.patch.yml +25 -0
- package/package.json +67 -4
- package/toolfold/toolfold.mjs +513 -0
- package/tools/agg-cost.mjs +78 -0
- package/tools/measure-prompt-surface.mjs +236 -0
- package/tools/session-econ.mjs +133 -0
- package/web/contextinjector.mjs +3139 -0
- package/web/extreason.mjs +1107 -0
- package/web/ledger-online.mjs +229 -0
- package/web/logcompiler.mjs +369 -0
- package/web/memory-durability.mjs +431 -0
- package/web/memory-janitor.mjs +551 -0
- package/web/memory-ledger-core.mjs +1331 -0
- package/web/memory-ledger.mjs +836 -0
- package/web/memory-observations.mjs +430 -0
- package/web/memory-online-bridge.mjs +688 -0
- package/web/memory-proposal-store.mjs +1054 -0
- package/web/memory-proposals.mjs +2306 -0
- package/web/memory-trace.mjs +533 -0
- package/web/memory-vec.mjs +241 -0
- package/web/memory.mjs +5801 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// 提示词体量与"转录在请求里到底有几份"的实测器(只读,零成本,不依赖 DSH 进程)
|
|
2
|
+
//
|
|
3
|
+
// 为什么存在(2026-09-14,付费跑批 A01/A0x 的分析事故):
|
|
4
|
+
// 有人把**追加式日志里"历史事件体的总和"**当成了"请求里重复注入的份数",
|
|
5
|
+
// 得出"转录被重复注入 ~29 次(2,020,741B / 70,269B = 28.8×)"的结论,并据此判定
|
|
6
|
+
// "折叠没有产生收益"。这个推论有三处硬错,本工具把三处都变成可复算的数字:
|
|
7
|
+
//
|
|
8
|
+
// ① 日志 ≠ 请求。单段转录每次折叠都**重写整份转录文本**(foldText + 新行),
|
|
9
|
+
// 日志(追加式)因此每次留下"当时全文";一个 32 轮会话有 2 折/轮 = 63 次提交,
|
|
10
|
+
// 累加自然 ≈ 2MB —— 那是**历史**,不是**上下文**。请求里只有一份:
|
|
11
|
+
// 插件每次提交后把 `session.deriveMessages()` 的真实消息结构写进 injector.jsonl
|
|
12
|
+
// 的 `after` 字段(role/src/bytes/hash),本工具直接统计它。
|
|
13
|
+
// ② 每折"第二份"只是同一份的**下一版**(增长 ~0.1%),不是并存的两份。
|
|
14
|
+
// ③ 用量算术可以直接否掉它:A01-off-1 实测 93 步,输入 token 合计
|
|
15
|
+
// uncached 80,655 + cacheRead 1,590,272 = 1,670,927 ≈ 1.67M。
|
|
16
|
+
// 若请求里真有 2.02MB(≈50 万 token)的转录,**单次请求**就已超过该总量,
|
|
17
|
+
// 93 步将需要 ~47M —— 实测值比"重复 29 份"的假设小 28 倍。
|
|
18
|
+
//
|
|
19
|
+
// 判据(`--check` 时任一不满足即 exit 1):
|
|
20
|
+
// I1 每次提交的请求里,**转录量级**(≥4KB)的 plugin 节点恰有 1 个(无累加、无丢失)
|
|
21
|
+
// I2 请求总字节 ≤ 最大单节点 + 8KB(没有"看不见的大块"藏在别处)
|
|
22
|
+
// I3 最后一份转录字节 == last-fold.full 字节(面板/索引/请求三者同源)
|
|
23
|
+
//
|
|
24
|
+
// 用法:
|
|
25
|
+
// node measure-prompt-surface.mjs --run <runDir> [--run <runDir> ...]
|
|
26
|
+
// node measure-prompt-surface.mjs --runs <父目录> # 自动扫 run-* 子目录
|
|
27
|
+
// node measure-prompt-surface.mjs --runs <父目录> --check # 不变量闸(CI/回归用)
|
|
28
|
+
// node measure-prompt-surface.mjs --runs <父目录> --json out.json
|
|
29
|
+
// 目录约定(付费跑批 harness 的落盘布局):
|
|
30
|
+
// <runDir>/out/injector.jsonl
|
|
31
|
+
// <runDir>/out/state/.ctxinjector/raw/session-<sid>.jsonl (折叠原料权威存证)
|
|
32
|
+
// <runDir>/out/state/.ctxinjector/last-fold/<sid>.json
|
|
33
|
+
import { readFileSync, readdirSync, existsSync, writeFileSync, statSync, mkdirSync } from 'node:fs'
|
|
34
|
+
import { join, basename, dirname } from 'node:path'
|
|
35
|
+
|
|
36
|
+
const argv = process.argv.slice(2)
|
|
37
|
+
const optAll = (name) => argv.reduce((acc, a, i) => (a === '--' + name && i + 1 < argv.length ? [...acc, argv[i + 1]] : acc), [])
|
|
38
|
+
const opt = (name, dflt = null) => (optAll(name)[0] ?? dflt)
|
|
39
|
+
const CHECK = argv.includes('--check')
|
|
40
|
+
const JSON_OUT = opt('json')
|
|
41
|
+
|
|
42
|
+
// ── 字节口径:必须与插件 messageBytes 逐字一致(否则本工具与 last-fold 对不上)──────
|
|
43
|
+
function blockTextOf(c) {
|
|
44
|
+
if (c === null || c === undefined) return ''
|
|
45
|
+
if (typeof c === 'string') return c
|
|
46
|
+
if (Array.isArray(c)) return c.map(blockTextOf).join('')
|
|
47
|
+
if (typeof c === 'object') {
|
|
48
|
+
if (typeof c.text === 'string') return c.text
|
|
49
|
+
if (c.content !== undefined) return blockTextOf(c.content)
|
|
50
|
+
try { return JSON.stringify(c) } catch { return '' }
|
|
51
|
+
}
|
|
52
|
+
return String(c)
|
|
53
|
+
}
|
|
54
|
+
function realBytes(m) {
|
|
55
|
+
const blocks = Array.isArray(m?.content) ? m.content : []
|
|
56
|
+
const parts = blocks.map((b) => {
|
|
57
|
+
switch (b.type) {
|
|
58
|
+
case 'text': return `text:${b.text ?? ''}`
|
|
59
|
+
case 'tool-call': return `tc:${b.name ?? ''}:${b.arguments ?? ''}`
|
|
60
|
+
case 'tool-result': return `tr:${b.name ?? b.toolCallId ?? ''}:${blockTextOf(b.content)}`
|
|
61
|
+
default: return `${b.type}:${JSON.stringify(b).slice(0, 120)}`
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
return `${m?.role ?? '?'}\u0001${parts.join('\u0001')}`.length
|
|
65
|
+
}
|
|
66
|
+
const readLines = (p) => {
|
|
67
|
+
if (!existsSync(p)) return []
|
|
68
|
+
try { return readFileSync(p, 'utf8').split('\n').filter((l) => l.trim()).map((l) => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean) } catch { return [] }
|
|
69
|
+
}
|
|
70
|
+
const findFile = (dir, name) => {
|
|
71
|
+
if (!existsSync(dir)) return null
|
|
72
|
+
const st = statSync(dir)
|
|
73
|
+
if (st.isFile()) return name.startsWith('.') ? (dir.endsWith(name) ? dir : null) : (basename(dir) === name ? dir : null)
|
|
74
|
+
const match = (f) => (name.startsWith('.') ? f.endsWith(name) : f === name)
|
|
75
|
+
for (const f of readdirSync(dir)) {
|
|
76
|
+
const p = join(dir, f)
|
|
77
|
+
if (match(f) && statSync(p).isFile()) return p
|
|
78
|
+
try { if (statSync(p).isDirectory()) { const q = findFile(p, name); if (q) return q } } catch { /* ignore */ }
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const TRANSCRIPT_SCALE = 4096 // ≥4KB 的 plugin 节点 = "转录量级"
|
|
84
|
+
|
|
85
|
+
function measureRun(runDir) {
|
|
86
|
+
const o = { run: basename(runDir), path: runDir, present: false, fails: [], warns: [], notes: [] }
|
|
87
|
+
const inj = existsSync(join(runDir, 'out', 'injector.jsonl')) ? join(runDir, 'out', 'injector.jsonl')
|
|
88
|
+
: (existsSync(join(runDir, 'injector.jsonl')) ? join(runDir, 'injector.jsonl') : null)
|
|
89
|
+
if (!inj) { o.notes.push('没有 injector.jsonl ⇒ 该臂未装载插件(bare 臂正常如此)'); return o }
|
|
90
|
+
o.present = true
|
|
91
|
+
const recs = readLines(inj)
|
|
92
|
+
const attempts = recs.filter((r) => r.tag === 'fold-attempt')
|
|
93
|
+
const commits = recs.filter((r) => r.tag === 'fold-committed')
|
|
94
|
+
o.attempts = attempts.length
|
|
95
|
+
o.commits = commits.length
|
|
96
|
+
if (commits.length === 0) { o.warns.push('没有任何 fold-committed 记录(该臂可能全程没折成)'); return o }
|
|
97
|
+
|
|
98
|
+
// 每次提交后的"真实请求结构":after(新)/ afterNodes(旧 last-fold 同名字段)
|
|
99
|
+
const snaps = commits.map((c) => (Array.isArray(c.after) ? c.after : (Array.isArray(c.afterNodes) ? c.afterNodes : [])))
|
|
100
|
+
.filter((a) => a.length > 0)
|
|
101
|
+
o.snapshots = snaps.length
|
|
102
|
+
const perCommit = snaps.map((nodes, i) => {
|
|
103
|
+
const live = nodes.reduce((a, n) => a + (Number(n.bytes) || 0), 0)
|
|
104
|
+
const plugins = nodes.filter((n) => n.src === 'plugin').map((n) => Number(n.bytes) || 0)
|
|
105
|
+
const big = plugins.filter((b) => b >= TRANSCRIPT_SCALE)
|
|
106
|
+
return { i, nodes: nodes.length, liveBytes: live, pluginCount: plugins.length, transBytes: plugins.length ? Math.max(...plugins) : 0, bigCount: big.length }
|
|
107
|
+
})
|
|
108
|
+
const last = perCommit[perCommit.length - 1]
|
|
109
|
+
o.requests = perCommit.length
|
|
110
|
+
o.finalNodes = last.nodes
|
|
111
|
+
o.finalLiveBytes = last.liveBytes
|
|
112
|
+
o.finalTranscriptBytes = last.transBytes
|
|
113
|
+
o.maxLiveBytes = Math.max(...perCommit.map((c) => c.liveBytes))
|
|
114
|
+
o.maxBigNodes = Math.max(...perCommit.map((c) => c.bigCount))
|
|
115
|
+
|
|
116
|
+
// ① 不变量:请求里**不会出现两份**转录量级节点(这就是"没有重复注入"的机械证据)。
|
|
117
|
+
// 注意早期折叠的转录还很小(<4KB)⇒ 只要求"至多 1 个",不要求恒为 1;末态另判。
|
|
118
|
+
for (const c of perCommit) {
|
|
119
|
+
if (c.bigCount > 1) o.fails.push(`第 ${c.i + 1} 次提交的请求里有 ${c.bigCount} 个转录量级(≥4KB) plugin 节点(至多 1 个 ⇒ 出现累加/重复注入)`)
|
|
120
|
+
if (c.liveBytes > c.transBytes + 8192) o.fails.push(`第 ${c.i + 1} 次提交:请求总字节 ${c.liveBytes} 超出最大单节点 ${c.transBytes} 达 ${c.liveBytes - c.transBytes}B(>8KB 余量,存在未计入的大块)`)
|
|
121
|
+
}
|
|
122
|
+
if (o.finalTranscriptBytes < TRANSCRIPT_SCALE) {
|
|
123
|
+
o.warns.push(`末态转录仅 ${o.finalTranscriptBytes}B(该臂几乎没跑起来/没折成)⇒ 本跑次的收益数字不具代表性`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ② 幻觉量:把"日志里每次提交的转录全文"累加 —— 复现 28.8× 那个数字,并给出它到底是什么
|
|
127
|
+
o.historySumBytes = perCommit.reduce((a, c) => a + c.transBytes, 0)
|
|
128
|
+
o.historyRatio = o.finalTranscriptBytes > 0 ? Number((o.historySumBytes / o.finalTranscriptBytes).toFixed(2)) : null
|
|
129
|
+
o.foldsPerTurn = o.commits
|
|
130
|
+
|
|
131
|
+
// ③ 请求里的"真实收益":末态请求 vs 不折叠时同一段对话的原文(raw 权威存证,按 msg.id 去重)
|
|
132
|
+
const rawDir = findFile(join(runDir, 'out', 'state', '.ctxinjector'), 'raw') ?? join(runDir, 'out', 'state', '.ctxinjector', 'raw')
|
|
133
|
+
const rawFile = existsSync(rawDir) ? readdirSync(rawDir).filter((f) => f.endsWith('.jsonl')).map((f) => join(rawDir, f))[0] : null
|
|
134
|
+
if (rawFile) {
|
|
135
|
+
const rows = readLines(rawFile)
|
|
136
|
+
const seen = new Map()
|
|
137
|
+
let ownBytes = 0
|
|
138
|
+
let ownRows = 0
|
|
139
|
+
for (const r of rows) {
|
|
140
|
+
const m = r?.msg
|
|
141
|
+
if (!m) continue
|
|
142
|
+
if (m?.source?.plugin === 'CONTEXTinjector') { ownRows++; ownBytes += realBytes(m); continue }
|
|
143
|
+
const key = m.id ?? JSON.stringify(m).slice(0, 200)
|
|
144
|
+
if (!seen.has(key)) seen.set(key, realBytes(m))
|
|
145
|
+
}
|
|
146
|
+
o.rawRows = rows.length
|
|
147
|
+
o.rawUniqueMsgs = seen.size
|
|
148
|
+
o.rawUniqueBytes = [...seen.values()].reduce((a, b) => a + b, 0)
|
|
149
|
+
o.rawOwnRows = ownRows
|
|
150
|
+
o.rawOwnBytes = ownBytes
|
|
151
|
+
o.rawOverLive = o.finalLiveBytes > 0 ? Number((o.rawUniqueBytes / o.finalLiveBytes).toFixed(2)) : null
|
|
152
|
+
o.rawOverTranscript = o.finalTranscriptBytes > 0 ? Number((o.rawUniqueBytes / o.finalTranscriptBytes).toFixed(2)) : null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ③' 与 last-fold 对账(面板/索引/请求三者同源)
|
|
156
|
+
const lfFile = findFile(join(runDir, 'out', 'state', '.ctxinjector', 'last-fold'), '.json')
|
|
157
|
+
if (lfFile) {
|
|
158
|
+
try {
|
|
159
|
+
const lf = JSON.parse(readFileSync(lfFile, 'utf8'))
|
|
160
|
+
o.lastFoldFullBytes = typeof lf.full === 'string' ? lf.full.length : null
|
|
161
|
+
o.plugin = lf.plugin
|
|
162
|
+
if (o.lastFoldFullBytes !== null && o.finalTranscriptBytes !== o.lastFoldFullBytes) {
|
|
163
|
+
o.fails.push(`末态请求里的转录 ${o.finalTranscriptBytes}B ≠ last-fold.full ${o.lastFoldFullBytes}B(请求与面板不同源)`)
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(lf.segments) && lf.segments.length > 1) o.notes.push(`segments=${lf.segments.length}(多段转录:每段各占一个节点,拼接才是全文)`)
|
|
166
|
+
} catch (e) { o.warns.push('last-fold 解析失败: ' + e.message) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ④ 正确口径的折叠压缩比(v1.10.25 起才有的字段;老记录没有则跳过)
|
|
170
|
+
const withRatio = attempts.filter((a) => Number.isFinite(a.foldRatio))
|
|
171
|
+
if (withRatio.length > 0) {
|
|
172
|
+
o.foldRatioMedian = Number(withRatio.map((a) => a.foldRatio).sort((x, y) => x - y)[Math.floor(withRatio.length / 2)].toFixed(4))
|
|
173
|
+
o.foldRatioMin = Math.min(...withRatio.map((a) => a.foldRatio))
|
|
174
|
+
o.foldRatioMax = Math.max(...withRatio.map((a) => a.foldRatio))
|
|
175
|
+
} else {
|
|
176
|
+
o.notes.push('没有 foldRatio 字段 ⇒ 该跑次用的是 v1.10.24 及更早(旧口径:shrink 结构上恒 ≈1,不含收益信息)')
|
|
177
|
+
}
|
|
178
|
+
const shrinks = attempts.map((a) => a.shrink).filter((x) => Number.isFinite(x))
|
|
179
|
+
if (shrinks.length > 0) o.shrinkMedian = Number(shrinks.sort((a, b) => a - b)[Math.floor(shrinks.length / 2)].toFixed(4))
|
|
180
|
+
return o
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const runs = []
|
|
184
|
+
const isEntry = process.argv[1] && import.meta.url.endsWith(basename(process.argv[1]))
|
|
185
|
+
if (isEntry) {
|
|
186
|
+
for (const r of optAll('run')) runs.push(r)
|
|
187
|
+
const parent = opt('runs')
|
|
188
|
+
if (parent) {
|
|
189
|
+
if (!existsSync(parent)) { console.error('找不到 ' + parent); process.exit(2) }
|
|
190
|
+
for (const d of readdirSync(parent)) {
|
|
191
|
+
const p = join(parent, d)
|
|
192
|
+
try { if (statSync(p).isDirectory() && /^run-/.test(d)) runs.push(p) } catch { /* ignore */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (runs.length === 0) { console.error('用法: node measure-prompt-surface.mjs --run <runDir> | --runs <父目录> [--check] [--json out.json]'); process.exit(2) }
|
|
196
|
+
|
|
197
|
+
const results = runs.map(measureRun)
|
|
198
|
+
const kb = (n) => (Number.isFinite(n) ? (n / 1024).toFixed(1) + 'K' : '-')
|
|
199
|
+
console.log('跑次'.padEnd(20) + '提交 请求节点 末态请求 末态转录 请求峰值 日志累加/末态 原料/末态 本折压缩比(中位) shrink(中位)')
|
|
200
|
+
for (const r of results) {
|
|
201
|
+
if (!r.present) { console.log(String(r.run).padEnd(20) + ' — (未装载插件)'); continue }
|
|
202
|
+
console.log(
|
|
203
|
+
String(r.run).padEnd(20) +
|
|
204
|
+
String(r.commits).padEnd(6) +
|
|
205
|
+
String(r.finalNodes ?? '-').padEnd(10) +
|
|
206
|
+
kb(r.finalLiveBytes).padEnd(11) +
|
|
207
|
+
kb(r.finalTranscriptBytes).padEnd(10) +
|
|
208
|
+
kb(r.maxLiveBytes).padEnd(10) +
|
|
209
|
+
String((r.historyRatio ?? '-') + 'x').padEnd(17) +
|
|
210
|
+
String((r.rawOverLive ?? '-') + 'x').padEnd(12) +
|
|
211
|
+
String(r.foldRatioMedian ?? '-').padEnd(17) +
|
|
212
|
+
String(r.shrinkMedian ?? '-')
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
for (const r of results) {
|
|
216
|
+
if (r.present && Number.isFinite(r.rawUniqueBytes)) {
|
|
217
|
+
console.log(`· ${r.run}: 不折叠时的同一段对话原文 = ${kb(r.rawUniqueBytes)}(${r.rawUniqueMsgs} 条,按 msg.id 去重;raw 存证 ${r.rawRows} 行含重复覆盖)` +
|
|
218
|
+
` ⇒ 末态请求 ${kb(r.finalLiveBytes)},末态转录 ${kb(r.finalTranscriptBytes)};折叠使"该显示的原料"降到 1/${r.rawOverLive ?? '-'}`)
|
|
219
|
+
}
|
|
220
|
+
for (const n of r.notes) console.log(`· ${r.run}: ${n}`)
|
|
221
|
+
for (const w of r.warns) console.log(`! ${r.run}: ${w}`)
|
|
222
|
+
for (const f of r.fails) console.log(`✗ ${r.run}: ${f}`)
|
|
223
|
+
}
|
|
224
|
+
const allFails = results.flatMap((r) => r.fails.map((f) => r.run + ': ' + f))
|
|
225
|
+
console.log(`\n不变量:每次提交的请求里"转录量级节点至多 1 个" + "请求总字节 ≈ 最大单节点" ⇒ ${allFails.length === 0 ? 'PASS' : 'FAIL ' + allFails.length}`)
|
|
226
|
+
console.log('说明:日志累加/末态 = 把每次提交的转录全文累加 ÷ 末态转录(= "重复注入 N 次"那种算法的产物,它不是请求体量);')
|
|
227
|
+
console.log(' 原料/末态 = 同一段对话不折叠时的原文 ÷ 末态请求体量(折叠的真实收益,0.47% 级别的日志增量与它无关)。')
|
|
228
|
+
if (JSON_OUT) {
|
|
229
|
+
try { mkdirSync(dirname(JSON_OUT), { recursive: true }) } catch { /* ignore */ }
|
|
230
|
+
writeFileSync(JSON_OUT, JSON.stringify(results, null, 2), 'utf8')
|
|
231
|
+
console.log('JSON → ' + JSON_OUT)
|
|
232
|
+
}
|
|
233
|
+
process.exit(CHECK && allFails.length > 0 ? 1 : 0)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { measureRun, realBytes, blockTextOf }
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// session-econ.mjs — 从本机 DSH 会话档案重算 README「实测」表。
|
|
3
|
+
//
|
|
4
|
+
// node tools/session-econ.mjs --dsh-home "$DSH_HOME" [--mode single] [--top 8] [--window 1048576]
|
|
5
|
+
//
|
|
6
|
+
// 口径(与 README 一致):
|
|
7
|
+
// 累计工具输出+回复 = 会话事件流里 tool/result + assistant/message + user/message 的原始字节
|
|
8
|
+
// 折叠后驻留 = .ctxinjector/last-fold/<sid>.json 的 transcriptBytes
|
|
9
|
+
// 上下文占用 = 每个 assistant/message 的 usage.inputTokens + usage.cacheReadTokens
|
|
10
|
+
//
|
|
11
|
+
// 会话档案是「多个 zstd 帧首尾相接」的 jsonl(每次追加一帧),Node 的
|
|
12
|
+
// zstdDecompressSync 只解第一帧,所以这里按帧魔数切片逐个解。
|
|
13
|
+
import { readdirSync, readFileSync, statSync, existsSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
16
|
+
|
|
17
|
+
const argv = process.argv.slice(2)
|
|
18
|
+
const arg = (name, def) => {
|
|
19
|
+
const i = argv.indexOf(name)
|
|
20
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : def
|
|
21
|
+
}
|
|
22
|
+
const HOME = arg('--dsh-home', process.env.DSH_HOME || join(process.env.USERPROFILE || '', '.dsh'))
|
|
23
|
+
const ONLY = arg('--mode', '') // 只看某个档位:single / double / off
|
|
24
|
+
const TOP = Number(arg('--top', '8'))
|
|
25
|
+
const WINDOW = Number(arg('--window', '1048576'))
|
|
26
|
+
const BYTES_PER_TOKEN = Number(arg('--bpt', '3.5'))
|
|
27
|
+
|
|
28
|
+
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
|
|
29
|
+
|
|
30
|
+
function decodeArchive(file) {
|
|
31
|
+
const buf = readFileSync(file)
|
|
32
|
+
const offs = []
|
|
33
|
+
for (let i = 0; (i = buf.indexOf(MAGIC, i)) !== -1; i += 4) offs.push(i)
|
|
34
|
+
let out = ''
|
|
35
|
+
for (let k = 0; k < offs.length; k++) {
|
|
36
|
+
const end = k + 1 < offs.length ? offs[k + 1] : buf.length
|
|
37
|
+
try { out += zstdDecompressSync(buf.subarray(offs[k], end)).toString('utf8') } catch { /* 半帧,跳过 */ }
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function walk(dir, out = []) {
|
|
43
|
+
let entries
|
|
44
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
|
|
45
|
+
for (const e of entries) {
|
|
46
|
+
const p = join(dir, e.name)
|
|
47
|
+
if (e.isDirectory()) walk(p, out)
|
|
48
|
+
else if (/^session.*\.jsonl\.zstd$/.test(e.name)) out.push(p)
|
|
49
|
+
}
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const med = (a) => {
|
|
54
|
+
if (!a.length) return 0
|
|
55
|
+
const s = [...a].sort((x, y) => x - y)
|
|
56
|
+
const m = s.length >> 1
|
|
57
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2
|
|
58
|
+
}
|
|
59
|
+
const pct = (a, p) => (a.length ? [...a].sort((x, y) => x - y)[Math.min(a.length - 1, Math.floor(a.length * p))] : 0)
|
|
60
|
+
|
|
61
|
+
// 档位(插件自己的记账)与折叠结果
|
|
62
|
+
const modes = {}
|
|
63
|
+
const ctlPath = join(HOME, 'state-compiler', 'control.json')
|
|
64
|
+
if (existsSync(ctlPath)) Object.assign(modes, JSON.parse(readFileSync(ctlPath, 'utf8')).sessionModes || {})
|
|
65
|
+
|
|
66
|
+
const foldDir = join(HOME, 'state-compiler', '.ctxinjector', 'last-fold')
|
|
67
|
+
const folds = {}
|
|
68
|
+
if (existsSync(foldDir)) {
|
|
69
|
+
for (const f of readdirSync(foldDir)) {
|
|
70
|
+
if (!f.endsWith('.json')) continue
|
|
71
|
+
try {
|
|
72
|
+
const j = JSON.parse(readFileSync(join(foldDir, f), 'utf8'))
|
|
73
|
+
folds[j.sessionId] = j
|
|
74
|
+
} catch { /* 坏记录忽略 */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rows = []
|
|
79
|
+
for (const file of walk(join(HOME, 'sessions'))) {
|
|
80
|
+
let text
|
|
81
|
+
try { text = decodeArchive(file) } catch { continue }
|
|
82
|
+
let sid = null, steps = 0, comp = 0, surface = 0
|
|
83
|
+
const ctxs = []
|
|
84
|
+
for (const line of text.split('\n')) {
|
|
85
|
+
if (!line) continue
|
|
86
|
+
let e
|
|
87
|
+
try { e = JSON.parse(line) } catch { continue }
|
|
88
|
+
if (e.type === 'session') { sid = e.id; continue }
|
|
89
|
+
if (e.type === 'step/start') steps++
|
|
90
|
+
else if (e.type === 'compaction/start') comp++
|
|
91
|
+
else if (e.type === 'tool/result' || e.type === 'assistant/message' || e.type === 'user/message') {
|
|
92
|
+
surface += JSON.stringify(e.data || {}).length
|
|
93
|
+
if (e.type === 'assistant/message' && e.data && e.data.usage) {
|
|
94
|
+
const u = e.data.usage
|
|
95
|
+
ctxs.push((u.inputTokens || 0) + (u.cacheReadTokens || 0))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!sid) continue
|
|
100
|
+
const mode = modes[sid] || null
|
|
101
|
+
if (ONLY && mode !== ONLY) continue
|
|
102
|
+
const fold = folds[sid] || null
|
|
103
|
+
rows.push({
|
|
104
|
+
sid, mode, steps, comp, surface,
|
|
105
|
+
folded: fold ? fold.transcriptBytes : null,
|
|
106
|
+
ctxMed: med(ctxs.filter((c) => c < 200000)),
|
|
107
|
+
ctxP90: pct(ctxs.filter((c) => c < 200000), 0.9),
|
|
108
|
+
ctxMax: ctxs.length ? Math.max(...ctxs) : 0,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
rows.sort((a, b) => b.surface - a.surface)
|
|
113
|
+
const withFold = rows.filter((r) => r.folded)
|
|
114
|
+
const sum = (a) => a.reduce((x, y) => x + y, 0)
|
|
115
|
+
const MB = (n) => (n / 1048576).toFixed(2)
|
|
116
|
+
const p = (n) => ((100 * n) / WINDOW).toFixed(1) + '%'
|
|
117
|
+
|
|
118
|
+
console.log(`DSH_HOME = ${HOME}`)
|
|
119
|
+
console.log(`会话 ${rows.length} 个(有折叠记录 ${withFold.length} 个)${ONLY ? `,只看档位 ${ONLY}` : ''}\n`)
|
|
120
|
+
console.log('| 会话 | 档位 | 步数 | 累计工具输出+回复 | 折叠后驻留 | 压缩比 | 上下文占用 中位 / P90 | 峰值 | 原生压缩 |')
|
|
121
|
+
console.log('|---|---|---|---|---|---|---|---|---|')
|
|
122
|
+
for (const r of withFold.slice(0, TOP)) {
|
|
123
|
+
console.log(`| \`${r.sid.replace('session-', '').slice(0, 8)}\` | ${r.mode || '-'} | ${r.steps} | ${MB(r.surface)} MB | ${MB(r.folded)} MB | **${(r.surface / r.folded).toFixed(0)}×** | ${p(r.ctxMed)} / ${p(r.ctxP90)} | ${p(r.ctxMax)} | ${r.comp} 次 |`)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (withFold.length) {
|
|
127
|
+
const surface = sum(withFold.map((r) => r.surface))
|
|
128
|
+
const folded = sum(withFold.map((r) => r.folded))
|
|
129
|
+
console.log(`\n合计 ${withFold.length} 个会话、${sum(withFold.map((r) => r.steps))} 步:`)
|
|
130
|
+
console.log(` 累计 ${MB(surface)} MB → 驻留 ${MB(folded)} MB(${(surface / folded).toFixed(0)}×)`)
|
|
131
|
+
console.log(` 按 ${BYTES_PER_TOKEN} B/token 折算:≈ ${(surface / BYTES_PER_TOKEN / 1e6).toFixed(1)}M token = ${(surface / BYTES_PER_TOKEN / WINDOW).toFixed(1)} 个窗口 → ${(folded / BYTES_PER_TOKEN / WINDOW).toFixed(2)} 个窗口`)
|
|
132
|
+
console.log(` 中位上下文占用:${p(med(withFold.map((r) => r.ctxMed)))};P90 中位:${p(med(withFold.map((r) => r.ctxP90)))}`)
|
|
133
|
+
}
|