@a9i5k4/dsh-auto-memory 2.2.2 → 2.2.4

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 (113) hide show
  1. package/docs/A3-RISK-ASSESSMENT-20260830.md +61 -0
  2. package/docs/COT-WATCH-RFC.md +88 -0
  3. package/docs/DUAL-TIER-RATIFICATION-PROMPT.md +71 -0
  4. package/docs/HANDOFF-HARNESS.md +78 -0
  5. package/docs/HANDOFF-M8-M9-M10.md +203 -0
  6. package/docs/HY4-TOUR-LOGO-HANDOFF-2.md +60 -0
  7. package/docs/HY4-TOUR-LOGO-HANDOFF.md +100 -0
  8. package/docs/ISSUE-REPLY-UNATTENDED.md +31 -0
  9. package/docs/K3-LANDING-HANDOFF.md +76 -0
  10. package/docs/LANDING-OUTLINE.md +85 -0
  11. package/docs/M-CM-PLAN.md +166 -0
  12. package/docs/M-CM-STATE.md +64 -0
  13. package/docs/M3B-CONTRACT.md +461 -0
  14. package/docs/M4-CONTRACT.md +1207 -0
  15. package/docs/M5-CONTRACT.md +383 -0
  16. package/docs/M6-CONTRACT.md +342 -0
  17. package/docs/M7-ACTIVATION-ALGO-REFERENCES.md +104 -0
  18. package/docs/M7-ACTIVATION-CALIBRATION.md +144 -0
  19. package/docs/M7-ACTIVATION-FEATURE-AGENT-PROMPT.md +79 -0
  20. package/docs/M7-ACTIVATION-FEATURE-CALIBRATION.md +58 -0
  21. package/docs/M7-ACTIVATION-FEATURE-DESIGN.md +124 -0
  22. package/docs/M7-ACTIVATION-V2-CONTROLLED-SHADOW.md +127 -0
  23. package/docs/M7-ACTIVATION-V2-HANDOFF.md +132 -0
  24. package/docs/M7-ACTIVATION-V2-HOLDEDOUT-EVAL.md +94 -0
  25. package/docs/M7-ACTIVATION-V2-HOLDEDOUT-SHADOW.md +60 -0
  26. package/docs/M7-ACTIVATION-V2-LIVE-SHADOW-PLAN.md +85 -0
  27. package/docs/M7-ACTIVATION-V2-PAPER.md +303 -0
  28. package/docs/M7-AGENT-HANDOFF-PROMPT.md +63 -0
  29. package/docs/M7-ALGORITHM-DECISION.md +174 -0
  30. package/docs/M7-AUTONOMOUS-STATE.md +252 -0
  31. package/docs/M7-BENCHMARK-PLAN.md +78 -0
  32. package/docs/M7-CLOSED-LOOP-WIRING.md +110 -0
  33. package/docs/M7-EMBEDDING-BENCHMARK.md +164 -0
  34. package/docs/M7-INTERFACE-DIGEST.md +144 -0
  35. package/docs/M7-LABEL-REVIEW-REPORT.md +107 -0
  36. package/docs/M7-LEXICAL-TUNING.md +58 -0
  37. package/docs/M7-LIVE-SHADOW-SCRIPT.md +30 -0
  38. package/docs/M7-PYTHON-IMPLEMENTATION-REPORT.md +92 -0
  39. package/docs/M7-RESEARCH-PAPER.md +442 -0
  40. package/docs/M7-TASKSET-DISPATCH.md +213 -0
  41. package/docs/M8-MEMORY-HUB.md +105 -0
  42. package/docs/MEMORY-SYSTEMS-SURVEY-2026-09.md +166 -0
  43. package/docs/NEXT-MAJOR-PROMO.md +363 -0
  44. package/docs/NEXT-MAJOR-README-DRAFT.zh.md +205 -0
  45. package/docs/NEXT-MAJOR-VISION.md +112 -0
  46. package/docs/PREVIEW-NEXT-STEPS.md +229 -0
  47. package/docs/PROJECT-FREEZE-AND-ROADMAP.md +200 -0
  48. package/docs/PROMO-STYLE-GUIDE.md +84 -0
  49. package/docs/PYTHON-SIDECAR-CONTRACT.md +539 -0
  50. package/docs/R2-POLICY-PLUMBING-BLUEPRINT.md +99 -0
  51. package/docs/RELEASE-READINESS-PLAN.md +91 -0
  52. package/docs/RELEASE-SEMANTIC-OPTION.md +181 -0
  53. package/docs/S1-SCIENTIFIC-RIGOR.md +269 -0
  54. package/docs/S2-DEEP-ABSORPTION.md +259 -0
  55. package/docs/S3-TARGET-ARCHITECTURE.md +172 -0
  56. package/docs/USER-GUIDE.zh-CN.md +283 -0
  57. package/docs/banner.jpg +0 -0
  58. package/docs/implementation-handoff-context.zh-CN.md +429 -0
  59. package/docs/landing/index.html +1745 -0
  60. package/docs/paper-figures/fig1_model_quality.png +0 -0
  61. package/docs/paper-figures/fig2_chunk_reversal.png +0 -0
  62. package/docs/paper-figures/fig3_latency.png +0 -0
  63. package/docs/paper-figures/fig4_hybrid.png +0 -0
  64. package/docs/paper-figures/fig5_rerank_tradeoff.png +0 -0
  65. package/docs/paper-figures/fig6_cluster_sweep.png +0 -0
  66. package/docs/paper-figures/fig7_resource.png +0 -0
  67. package/docs/paper-figures-v2/fig1_echo_trap.png +0 -0
  68. package/docs/paper-figures-v2/fig2_pr_paths.png +0 -0
  69. package/docs/paper-figures-v2/fig3_coefficients.png +0 -0
  70. package/docs/paper-figures-v2/fig4_calibration.png +0 -0
  71. package/docs/paper-figures-v2/fig5_order_ablation.png +0 -0
  72. package/docs/paper-figures-v2/fig6_containment.png +0 -0
  73. package/docs/proactive-associative-memory-architecture.html +659 -0
  74. package/docs/proactive-associative-memory-meta-code.html +1058 -0
  75. package/docs/proactive-associative-memory-research-report.zh-CN.md +685 -0
  76. package/docs/proactive-associative-memory-system-map.html +1580 -0
  77. package/docs/promo/first-run-guide.html +397 -0
  78. package/docs/promo/homepage.html +384 -0
  79. package/docs/screenshots/calendar-en.png +0 -0
  80. package/docs/screenshots/calendar-zh.png +0 -0
  81. package/docs/screenshots/connect-en.png +0 -0
  82. package/docs/screenshots/connect-zh.png +0 -0
  83. package/docs/screenshots/main-connect-en.png +0 -0
  84. package/docs/screenshots/main-connect-zh.png +0 -0
  85. package/docs/screenshots/overview-en.png +0 -0
  86. package/docs/screenshots/overview-zh.png +0 -0
  87. package/docs/screenshots/panel-hub.png +0 -0
  88. package/docs/screenshots/panel-overview.png +0 -0
  89. package/docs/screenshots/panel-refine.png +0 -0
  90. package/docs/screenshots/promo/promo-0-banner-v2.png +0 -0
  91. package/docs/screenshots/promo/promo-1-hero.png +0 -0
  92. package/docs/screenshots/promo/promo-2-tour.png +0 -0
  93. package/docs/screenshots/promo/promo-3-recall.png +0 -0
  94. package/docs/screenshots/promo/promo-4-unattended.png +0 -0
  95. package/docs/screenshots/promo/promo-5-external.png +0 -0
  96. package/docs/screenshots/promo/promo-6-greeting.png +0 -0
  97. package/docs/screenshots/reflections-en.png +0 -0
  98. package/docs/screenshots/search-zh.png +0 -0
  99. package/docs/screenshots/settings-2-zh.png +0 -0
  100. package/docs/screenshots/settings-debug-zh.png +0 -0
  101. package/docs/screenshots/settings-en.png +0 -0
  102. package/docs/screenshots/settings-zh.png +0 -0
  103. package/docs/screenshots/tour-core.png +0 -0
  104. package/docs/screenshots/tour-external.png +0 -0
  105. package/docs/screenshots/tour-toggles.png +0 -0
  106. package/docs/screenshots/tour-welcome.png +0 -0
  107. package/docs/screenshots/workspace-map-zh.png +0 -0
  108. package/docs/social-preview.png +0 -0
  109. package/lib/client.js +277 -118
  110. package/lib/index.js +329 -60
  111. package/lib/subagent-gc.js +239 -0
  112. package/lib/water-window.js +88 -0
  113. package/package.json +2 -1
@@ -0,0 +1,239 @@
1
+ /** 子代理痕迹回收(pre 线,2026-09-08)。
2
+ *
3
+ * 背景(实测 2026-09-08):DSH 会为每个子代理创建持久化会话
4
+ * `~/.dsh/sessions/<工作区>/<裸 uuid>/session.jsonl.zstd`,header 形如
5
+ * `{origin:'subagent', parentSession, delegationDepth}`;一次性委派结束后文件仍留在磁盘。
6
+ * 本插件的 auto-memory-* 子代理(自动沉淀 / 时段总结 / 问候 / 蒸馏 / 关键词)高频产生:
7
+ * 实测全机 686 个子代理会话(302MB)中 638 个(93%)来自本插件,数量上千后拖慢会话列表
8
+ * 与投影缓存加载,并可能触发子代理目录诊断报错。
9
+ *
10
+ * 本模块只做两件事,边界清晰、可单测:
11
+ * 1) 扫描:识别「本插件产生的、已结束的一次性子代理会话」
12
+ * (origin==='subagent' && descriptor.label 以 auto-memory- 开头 && descriptor.mode==='one-shot');
13
+ * 2) 回收:把会话目录 + 投影缓存文件**移动**到备份目录(不做不可逆删除,可整体回滚)。
14
+ *
15
+ * 安全边界:
16
+ * - 绝不触碰普通会话(目录名以 session- 开头 / header.origin !== 'subagent');
17
+ * - continuable 子代理(可续聊)一律跳过;
18
+ * - keepMs 之内新产生的会话跳过(避免与运行中的子代理竞争);
19
+ * - 任一步失败只记录,不抛错(调用方 fail-soft)。
20
+ */
21
+
22
+ import { readdir, stat, rename, mkdir, access } from 'node:fs/promises'
23
+ import { readFile } from 'node:fs/promises'
24
+ import { zstdDecompressSync } from 'node:zlib'
25
+ import path from 'node:path'
26
+
27
+ /** 本插件 spawn 的子代理 label 前缀(runSubagent 的 label 参数)。 */
28
+ export const PLUGIN_LABEL_PREFIX = 'auto-memory-'
29
+
30
+ /** 默认保留时长:3 天(兜底 GC 用;任务结束即删路径不受此限制)。 */
31
+ export const DEFAULT_KEEP_MS = 3 * 24 * 60 * 60 * 1000
32
+
33
+ /**
34
+ * 按块头定位多帧 zstd 文件的每个帧区间。
35
+ * 会话日志是逐事件追加的独立 zstd 帧,单次 zstdDecompressSync 只解第一帧。
36
+ * @param {Buffer} buf - 原始文件内容。
37
+ * @returns {{start:number,end:number}[]} 帧区间。
38
+ */
39
+ export function scanZstdFrames(buf) {
40
+ const frames = []
41
+ let off = 0
42
+ while (off + 4 <= buf.length && buf.readUInt32LE(off) === 0xfd2fb528) {
43
+ const start = off
44
+ const fhd = buf.readUInt8(off + 4)
45
+ const fcsFlag = fhd >> 6
46
+ const single = (fhd >> 5) & 1
47
+ const dictFlag = fhd & 3
48
+ const cksum = (fhd >> 2) & 1
49
+ let p = off + 5
50
+ if (!single) p += 1
51
+ if (dictFlag === 1) p += 1
52
+ else if (dictFlag === 2) p += 2
53
+ else if (dictFlag === 3) p += 4
54
+ if (fcsFlag === 0) { if (single) p += 1 }
55
+ else if (fcsFlag === 1) p += 2
56
+ else if (fcsFlag === 2) p += 4
57
+ else p += 8
58
+ off = p
59
+ let broken = false
60
+ for (;;) {
61
+ if (off + 3 > buf.length) { broken = true; break }
62
+ const bh = buf.readUIntLE(off, 3)
63
+ off += 3
64
+ const last = bh & 1
65
+ const btype = (bh >> 1) & 3
66
+ let bsize = bh >>> 3
67
+ if (btype === 1) bsize = 1
68
+ else if (btype === 3) { broken = true; break }
69
+ off += bsize
70
+ if (last) break
71
+ }
72
+ if (cksum) off += 4
73
+ if (broken && off > buf.length) break
74
+ frames.push({ start, end: Math.min(off, buf.length) })
75
+ if (off >= buf.length) break
76
+ }
77
+ return frames
78
+ }
79
+
80
+ /**
81
+ * 多帧 zstd 全量解压为文本(单帧文件同样适用)。
82
+ * @param {Buffer} buf - 原始文件内容。
83
+ * @returns {string} 解压文本(坏帧跳过)。
84
+ */
85
+ export function decodeZstdFrames(buf) {
86
+ const parts = []
87
+ for (const fr of scanZstdFrames(buf)) {
88
+ try { parts.push(zstdDecompressSync(buf.subarray(fr.start, fr.end))) } catch (e) { /* 坏帧跳过 */ }
89
+ }
90
+ return Buffer.concat(parts).toString('utf8')
91
+ }
92
+
93
+ /**
94
+ * 从会话日志文本里取首帧 header 与 subagent descriptor。
95
+ * @param {string} text - 解压后的 JSONL 文本。
96
+ * @returns {{header:object|null,descriptor:{label?:string,mode?:string}|null,eventCount:number}}
97
+ */
98
+ export function parseSessionHead(text) {
99
+ let header = null
100
+ let descriptor = null
101
+ let eventCount = 0
102
+ for (const ln of String(text).split('\n')) {
103
+ if (!ln) continue
104
+ let ev = null
105
+ try { ev = JSON.parse(ln) } catch (e) { continue }
106
+ eventCount++
107
+ if (ev.type === 'session' && header === null) header = ev
108
+ else if (ev.type === 'subagent/descriptor' && descriptor === null && ev.data && typeof ev.data === 'object') {
109
+ descriptor = { label: ev.data.label, mode: ev.data.mode }
110
+ }
111
+ if (header !== null && descriptor !== null) break
112
+ }
113
+ return { header, descriptor, eventCount }
114
+ }
115
+
116
+ /**
117
+ * 判定一个会话目录是否属于「本插件产生的一次性子代理痕迹」。
118
+ * @param {{header:object|null,descriptor:object|null}} head - parseSessionHead 的结果。
119
+ * @param {{labelPrefix?:string,keepMs?:number,now?:number,mtimeMs?:number}} opts - 判据。
120
+ * @returns {{hit:boolean,reason:string,label:string,mode:string}}
121
+ */
122
+ export function isPluginOneShotSubagent(head, opts = {}) {
123
+ const labelPrefix = opts.labelPrefix || PLUGIN_LABEL_PREFIX
124
+ const keepMs = Number.isFinite(opts.keepMs) ? Number(opts.keepMs) : DEFAULT_KEEP_MS
125
+ const now = Number.isFinite(opts.now) ? Number(opts.now) : Date.now()
126
+ const header = head && head.header
127
+ const descriptor = head && head.descriptor
128
+ const label = String((descriptor && descriptor.label) || '')
129
+ const mode = String((descriptor && descriptor.mode) || '')
130
+ if (!header) return { hit: false, reason: 'no-header', label, mode }
131
+ if (header.origin !== 'subagent') return { hit: false, reason: 'not-subagent', label, mode }
132
+ if (header.parentSession === undefined || header.parentSession === null || header.parentSession === '') {
133
+ return { hit: false, reason: 'no-parent', label, mode }
134
+ }
135
+ if (label === '' || !label.startsWith(labelPrefix)) return { hit: false, reason: 'foreign-label', label, mode }
136
+ if (mode === 'continuable') return { hit: false, reason: 'continuable', label, mode }
137
+ if (mode !== 'one-shot') return { hit: false, reason: 'unknown-mode', label, mode }
138
+ const mt = Number(opts.mtimeMs || 0)
139
+ if (keepMs > 0 && mt > 0 && now - mt < keepMs) return { hit: false, reason: 'too-recent', label, mode }
140
+ return { hit: true, reason: 'hit', label, mode }
141
+ }
142
+
143
+ /**
144
+ * 扫描会话根目录,列出可回收的本插件子代理会话。
145
+ * @param {{sessionsRoot:string,labelPrefix?:string,keepMs?:number,now?:number,maxEntries?:number}} opts - 扫描参数。
146
+ * @returns {Promise<{candidates:object[],scanned:number,subagents:number,bytes:number,reasons:Record<string,number>}>}
147
+ */
148
+ export async function scanPluginSubagentSessions(opts = {}) {
149
+ const sessionsRoot = opts.sessionsRoot
150
+ if (!sessionsRoot) throw new Error('scanPluginSubagentSessions: sessionsRoot required')
151
+ const labelPrefix = opts.labelPrefix || PLUGIN_LABEL_PREFIX
152
+ const keepMs = Number.isFinite(opts.keepMs) ? Number(opts.keepMs) : DEFAULT_KEEP_MS
153
+ const now = Number.isFinite(opts.now) ? Number(opts.now) : Date.now()
154
+ const maxEntries = Number(opts.maxEntries || 20000)
155
+ const out = { candidates: [], scanned: 0, subagents: 0, bytes: 0, reasons: {} }
156
+ const bump = (r) => { out.reasons[r] = (out.reasons[r] || 0) + 1 }
157
+ let wsDirs = []
158
+ try { wsDirs = await readdir(sessionsRoot, { withFileTypes: true }) } catch (e) { return out }
159
+ for (const ws of wsDirs) {
160
+ if (!ws.isDirectory()) continue
161
+ const wsDir = ws.name
162
+ let entries = []
163
+ try { entries = await readdir(path.join(sessionsRoot, wsDir), { withFileTypes: true }) } catch (e) { continue }
164
+ for (const ent of entries) {
165
+ if (!ent.isDirectory()) continue
166
+ if (out.scanned >= maxEntries) break
167
+ out.scanned++
168
+ const sid = ent.name
169
+ const dir = path.join(sessionsRoot, wsDir, sid)
170
+ const file = path.join(dir, 'session.jsonl.zstd')
171
+ let st = null
172
+ try { st = await stat(file) } catch (e) { bump('no-file'); continue }
173
+ let text = ''
174
+ try { text = decodeZstdFrames(await readFile(file)) } catch (e) { bump('decode-error'); continue }
175
+ const head = parseSessionHead(text)
176
+ if (!head.header) { bump('no-header'); continue }
177
+ if (head.header.origin === 'subagent') out.subagents++
178
+ const verdict = isPluginOneShotSubagent(head, { labelPrefix, keepMs, now, mtimeMs: st.mtimeMs })
179
+ bump(verdict.reason)
180
+ if (!verdict.hit) continue
181
+ out.candidates.push({
182
+ wsDir,
183
+ sid,
184
+ dir,
185
+ file,
186
+ label: verdict.label,
187
+ mode: verdict.mode,
188
+ parentSession: String(head.header.parentSession || ''),
189
+ delegationDepth: Number(head.header.delegationDepth || 0),
190
+ sizeBytes: st.size,
191
+ mtimeMs: st.mtimeMs,
192
+ })
193
+ out.bytes += st.size
194
+ }
195
+ }
196
+ out.candidates.sort((a, b) => a.mtimeMs - b.mtimeMs)
197
+ return out
198
+ }
199
+
200
+ /** 目标路径是否已存在。 */
201
+ async function exists(p) {
202
+ try { await access(p); return true } catch (e) { return false }
203
+ }
204
+
205
+ /**
206
+ * 把候选会话移动到备份目录(可回滚),并顺带移走对应的投影缓存文件。
207
+ * 不做任何删除;同名冲突时跳过并记录。
208
+ * @param {{candidates:object[],backupRoot:string,projcacheRoot?:string,apply?:boolean}} opts - 回收参数。
209
+ * @returns {Promise<{moved:object[],skipped:object[],failed:object[],bytes:number,applied:boolean}>}
210
+ */
211
+ export async function recycleSessions(opts = {}) {
212
+ const apply = opts.apply === true
213
+ const backupRoot = opts.backupRoot
214
+ const projcacheRoot = opts.projcacheRoot || ''
215
+ const out = { moved: [], skipped: [], failed: [], bytes: 0, applied: apply }
216
+ if (!backupRoot) throw new Error('recycleSessions: backupRoot required')
217
+ for (const c of opts.candidates || []) {
218
+ const destDir = path.join(backupRoot, c.wsDir || '_', c.sid)
219
+ try {
220
+ if (await exists(destDir)) { out.skipped.push({ sid: c.sid, reason: 'backup-exists' }); continue }
221
+ if (!apply) { out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir, dryRun: true }); out.bytes += c.sizeBytes; continue }
222
+ await mkdir(path.dirname(destDir), { recursive: true })
223
+ await rename(c.dir, destDir)
224
+ out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir })
225
+ out.bytes += c.sizeBytes
226
+ if (projcacheRoot) {
227
+ const cacheFile = path.join(projcacheRoot, c.sid)
228
+ if (await exists(cacheFile)) {
229
+ const cacheDest = path.join(backupRoot, '_projcache', c.sid)
230
+ await mkdir(path.dirname(cacheDest), { recursive: true })
231
+ await rename(cacheFile, cacheDest).catch(() => {})
232
+ }
233
+ }
234
+ } catch (e) {
235
+ out.failed.push({ sid: c.sid, error: e && e.message ? e.message : String(e) })
236
+ }
237
+ }
238
+ return out
239
+ }
@@ -0,0 +1,88 @@
1
+ /** 上下文窗口解析(2.2.4,2026-09-08)。
2
+ *
3
+ * 背景:水位 = 当前上下文占用 / 模型窗口。占用取官方 tokenMeter(与聊天框 context ring 同源),
4
+ * 窗口此前有三条来源:手动覆盖 > 官方 request/context 的 contextWindow > settings.yaml 解析 > fallback。
5
+ * 实测发现两条路同时失效,导致窗口恒为 fallback 131072(真实模型 1M),水位被算成 150%+:
6
+ * 1) 官方 request/context 只在会话早期追加(本会话 2999 个事件里仅 2 条),而旧代码只扫最近 256 条 → 永远扫不到;
7
+ * 2) settings.yaml 的 llm-deepseek 段是 flow 风格 YAML(`{ models: [ { id: x, contextWindow: 1000000, ... } ] }`),
8
+ * 旧解析器只认 block 风格(`- id: x` + 行尾纯数字),整段解析不出来。
9
+ *
10
+ * 本模块把「settings.yaml → 模型窗口映射」与「按 provider/model 选窗口」抽成纯函数,便于 smoke 覆盖。
11
+ */
12
+
13
+ /**
14
+ * 从 settings.yaml 文本解析 `模型 id → contextWindow` 映射。
15
+ * 同时支持 block 风格(llm-pi-ai 那种 `- id: x` / `contextWindow: N`)与 flow 风格(llm-deepseek 那种 `id: x,` / `contextWindow: N,`)。
16
+ * @param {string} text - settings.yaml 原文。
17
+ * @returns {{byRoute:Record<string,number>,byModel:Record<string,number>}} byRoute 键为 provider/id,byModel 键为 id。
18
+ */
19
+ export function parseModelWindowsPre(text) {
20
+ const byRoute = {}
21
+ const byModel = {}
22
+ if (!text || typeof text !== 'string') return { byRoute, byModel }
23
+ const lines = String(text).split(/\r?\n/)
24
+ let currentProvider = ''
25
+ let currentId = null
26
+ for (const line of lines) {
27
+ // provider 段(block 风格,2 空格缩进的裸键,如 `opencode-go:`);flow 风格段名同样以冒号结尾但缩进更深
28
+ const pm = line.match(/^ {2}([A-Za-z0-9_.-]+):\s*$/)
29
+ if (pm) { currentProvider = pm[1]; currentId = null; continue }
30
+ // 模型 id:兼容 block(`- id: x`)与 flow(`id: x,`)两种写法,允许行尾逗号
31
+ const im = line.match(/^\s*(?:-\s*)?id:\s*([A-Za-z0-9._:-]+?)\s*,?\s*$/)
32
+ if (im) { currentId = im[1]; if (byModel[currentId] === undefined) byModel[currentId] = 0; continue }
33
+ // 窗口:允许行尾逗号(flow 风格)
34
+ const cm = line.match(/^\s*contextWindow:\s*([0-9]+)\s*,?\s*$/)
35
+ if (cm && currentId) {
36
+ const value = Number(cm[1])
37
+ const route = currentProvider + '/' + currentId
38
+ if (byRoute[route] === undefined) byRoute[route] = value
39
+ if (!byModel[currentId]) byModel[currentId] = value
40
+ }
41
+ }
42
+ return { byRoute, byModel }
43
+ }
44
+
45
+ /**
46
+ * 按 provider/model 选窗口:先精确匹配 provider/model,再按 model 名全局兜底。
47
+ * @param {{byRoute:Record<string,number>,byModel:Record<string,number>}} windows - parseModelWindowsPre 结果。
48
+ * @param {string} provider - 路由 provider(可空)。
49
+ * @param {string} model - 模型 id。
50
+ * @returns {number} 窗口 token 数;未命中返回 0。
51
+ */
52
+ export function pickWindowPre(windows, provider, model) {
53
+ if (!windows || !model) return 0
54
+ const byRoute = windows.byRoute || {}
55
+ const byModel = windows.byModel || {}
56
+ const exact = byRoute[(provider || '') + '/' + model]
57
+ if (Number.isFinite(exact) && exact > 0) return exact
58
+ const direct = byModel[model]
59
+ if (Number.isFinite(direct) && direct > 0) return direct
60
+ // 后缀兜底:路由 id 可能带 provider 前缀或版本后缀差异
61
+ for (const route of Object.keys(byRoute)) {
62
+ if (route.endsWith('/' + model) && byRoute[route] > 0) return byRoute[route]
63
+ }
64
+ for (const id of Object.keys(byModel)) {
65
+ if (id.endsWith('-' + model) && byModel[id] > 0) return byModel[id]
66
+ }
67
+ return 0
68
+ }
69
+
70
+ /**
71
+ * 从会话事件里取最近一条 request/context 的 contextWindow(路由权威值)。
72
+ * 不限制扫描窗口:该事件只在请求头部变化时追加,长会话里往往只存在于开头。
73
+ * @param {object[]} events - 会话事件数组。
74
+ * @param {number} maxScan - 最多向前扫多少条(默认全部;设小值可限制开销)。
75
+ * @returns {number} contextWindow;未找到返回 0。
76
+ */
77
+ export function findOfficialContextWindowPre(events, maxScan = 0) {
78
+ if (!Array.isArray(events) || !events.length) return 0
79
+ const floor = maxScan > 0 ? Math.max(0, events.length - maxScan) : 0
80
+ for (let i = events.length - 1; i >= floor; i--) {
81
+ const ev = events[i]
82
+ if (ev && ev.type === 'request/context') {
83
+ const cw = Number(ev && ev.data && ev.data.contextWindow)
84
+ return Number.isFinite(cw) && cw > 0 ? cw : 0
85
+ }
86
+ }
87
+ return 0
88
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-auto-memory",
3
3
  "description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
4
- "version": "2.2.2",
4
+ "version": "2.2.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "lib",
14
14
  "python",
15
+ "docs",
15
16
  "cordis.patch.yml",
16
17
  "!python/bench",
17
18
  "!python/__pycache__"