@a9i5k4/dsh-auto-memory 2.5.3 → 3.0.1

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 (167) hide show
  1. package/README.md +189 -7
  2. package/README.zh-CN.md +189 -7
  3. package/docs/CONTRIBUTORS.html +471 -0
  4. package/docs/FRONTEND-CO-CREATION.md +191 -0
  5. package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
  6. package/docs/HANDOFF-CRITERIA.md +92 -0
  7. package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
  8. package/docs/INTEGRATION-ANALYSIS.md +350 -348
  9. package/docs/PROMO-PROMPT-3.0.md +100 -0
  10. package/docs/USER-GUIDE.en.md +58 -3
  11. package/docs/USER-GUIDE.zh-CN.md +59 -4
  12. package/docs/WHITEPAPER.md +207 -0
  13. package/docs/internal/ACCEPT-35-LIVE.md +143 -0
  14. package/docs/internal/ACCEPTANCE-20260914.md +90 -0
  15. package/docs/internal/ARCH-REVIEW-BRIEF.md +411 -0
  16. package/docs/internal/ARCH-REVIEW-REQUEST.md +201 -0
  17. package/docs/internal/ARCH-REVIEW-ROUND2.md +169 -0
  18. package/docs/internal/ARCH-REVIEW-ROUND3.md +206 -0
  19. package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +397 -0
  20. package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +351 -0
  21. package/docs/internal/ART-DIRECTION-WIREFRAME.md +191 -181
  22. package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +181 -0
  23. package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +314 -0
  24. package/docs/internal/BATTLE-PLAN-20260917.md +871 -0
  25. package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +192 -0
  26. package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +72 -0
  27. package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +131 -0
  28. package/docs/internal/DECISIONS-20260914-SESSION.md +269 -0
  29. package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +219 -0
  30. package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +132 -0
  31. package/docs/internal/FEATURE-INVENTORY.md +531 -0
  32. package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +13 -0
  33. package/docs/internal/G-SERIES-EXECUTION-20260917.md +248 -0
  34. package/docs/internal/G3-DESIGN-20260918.md +82 -0
  35. package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +92 -0
  36. package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +74 -0
  37. package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +352 -0
  38. package/docs/internal/GPT-REVIEW-PROMPT.md +216 -0
  39. package/docs/internal/GROUP-WEBHOOK-SETUP.md +33 -0
  40. package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +309 -0
  41. package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +120 -0
  42. package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +74 -0
  43. package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +175 -0
  44. package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +389 -0
  45. package/docs/internal/ISSUE10-PLAN-20260919.md +254 -0
  46. package/docs/internal/ISSUE10B-FORENSICS-20260919.md +468 -0
  47. package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +150 -0
  48. package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +114 -0
  49. package/docs/internal/KICKOFF-P0.md +254 -0
  50. package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +79 -0
  51. package/docs/internal/MASTER-PLAN-3.0.md +411 -0
  52. package/docs/internal/MEMORY-GOVERNANCE-20260917.md +309 -0
  53. package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +85 -0
  54. package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +222 -0
  55. package/docs/internal/PENDING-FIXES-20260916.md +289 -0
  56. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +705 -0
  57. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +649 -0
  58. package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +225 -0
  59. package/docs/internal/PROGRESS-20260917.md +93 -0
  60. package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +128 -0
  61. package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +163 -0
  62. package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +127 -0
  63. package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +140 -0
  64. package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +138 -0
  65. package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +218 -0
  66. package/docs/internal/RAG-KARPATHY-PROGRAM.md +229 -0
  67. package/docs/internal/REPORT-P0-NIGHTLY.md +212 -0
  68. package/docs/internal/REPORT-P5-ACCEPTANCE.md +31 -0
  69. package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +153 -0
  70. package/docs/internal/RESUME-20260918.md +171 -0
  71. package/docs/internal/RESUME-20260919.md +104 -0
  72. package/docs/internal/REVIEW-WB-GRAPH-SELF.md +81 -0
  73. package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +198 -0
  74. package/docs/internal/ROADMAP-20260917-WEEK.md +439 -0
  75. package/docs/internal/ROADMAP.md +106 -0
  76. package/docs/internal/RUN-P0-NIGHTLY.md +227 -0
  77. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +185 -0
  78. package/docs/internal/S10-GAP-INVENTORY-20260917.md +239 -0
  79. package/docs/internal/S10-GAPS-PLAIN-20260917.md +125 -0
  80. package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +360 -0
  81. package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +90 -0
  82. package/docs/internal/T6-EXECUTION-20260920.md +130 -0
  83. package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +146 -0
  84. package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +89 -0
  85. package/docs/internal/THESIS-OUTLINE-20260918.md +147 -0
  86. package/docs/internal/THREE-LAYER-CONTRACT.md +219 -0
  87. package/docs/internal/TODO-BACKLOG.md +263 -142
  88. package/docs/internal/TODO-GRAPH.html +715 -0
  89. package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +493 -0
  90. package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +710 -0
  91. package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +710 -0
  92. package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +703 -0
  93. package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +710 -0
  94. package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +715 -0
  95. package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +297 -0
  96. package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +104 -0
  97. package/docs/internal/WB-FORMAT-CONVENTION.md +112 -0
  98. package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +71 -0
  99. package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +56 -0
  100. package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +787 -0
  101. package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +112 -0
  102. package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +230 -0
  103. package/docs/prompts/M8-3-enable-verify.md +49 -49
  104. package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
  105. package/lib/acceptance.js +71 -0
  106. package/lib/activation-host.js +153 -18
  107. package/lib/activation-inbox.js +25 -7
  108. package/lib/board-mode.js +30 -0
  109. package/lib/client.js +1758 -90
  110. package/lib/config-io.js +156 -0
  111. package/lib/context-bridge.js +5 -2
  112. package/lib/context-host.js +86 -15
  113. package/lib/degrade.js +385 -0
  114. package/lib/dsh-home.js +143 -0
  115. package/lib/engine-identity.js +149 -0
  116. package/lib/engine-switch.js +247 -0
  117. package/lib/episodic-store.js +63 -12
  118. package/lib/evidence-store.js +10 -3
  119. package/lib/fact-store.js +22 -3
  120. package/lib/fs-retry.js +46 -0
  121. package/lib/index-sync.js +13 -1
  122. package/lib/index.js +3446 -263
  123. package/lib/intent-clean-safe.js +258 -0
  124. package/lib/intent-clean.js +12 -16
  125. package/lib/l0-extract.js +478 -149
  126. package/lib/l0-index-sync.js +195 -0
  127. package/lib/l0-index.js +349 -239
  128. package/lib/ledger-criteria.js +142 -0
  129. package/lib/m4-corpus.js +8 -2
  130. package/lib/m7-index-sync-host.js +73 -5
  131. package/lib/m7-wire.js +3 -3
  132. package/lib/memory-anchor.js +56 -1
  133. package/lib/memory-envelope.js +257 -0
  134. package/lib/memory-hub.js +138 -13
  135. package/lib/memory-index.js +4 -2
  136. package/lib/memory-mutation.js +246 -0
  137. package/lib/memory-writer.js +204 -24
  138. package/lib/note-status-apply.js +118 -0
  139. package/lib/note-status.js +196 -0
  140. package/lib/procedure-observation.js +48 -0
  141. package/lib/procedure-store.js +118 -20
  142. package/lib/python-setup.js +1 -1
  143. package/lib/python-sidecar-client.js +29 -3
  144. package/lib/recall-fusion.js +83 -12
  145. package/lib/rerank-host.js +160 -0
  146. package/lib/rules-edit.js +159 -0
  147. package/lib/rules-layer.js +261 -0
  148. package/lib/semantic-decide.js +41 -8
  149. package/lib/semantic-js.js +66 -6
  150. package/lib/shadow-host.js +3 -5
  151. package/lib/shadow-retrieval.js +3 -3
  152. package/lib/skill-export-host.js +153 -0
  153. package/lib/skill-export.js +239 -0
  154. package/lib/state-commit.js +245 -0
  155. package/lib/storage-manage.js +6 -0
  156. package/lib/subagent-gc.js +4 -8
  157. package/lib/temporal-parse.js +191 -159
  158. package/lib/tier-layer-inject.js +650 -0
  159. package/lib/tier0-catalog.js +735 -0
  160. package/lib/water-window.js +263 -186
  161. package/lib/wb-contract.js +691 -0
  162. package/lib/wb-sidecar.js +890 -0
  163. package/lib/ws-overview-rank.js +2 -2
  164. package/package.json +1 -1
  165. package/python/m7_embedding_v1.py +5 -5
  166. package/python/worker_semantic_v1.py +17 -6
  167. package/python/worker_v1.py +38 -4
@@ -0,0 +1,247 @@
1
+ /**
2
+ * 引擎切换状态机(engine_switch_v1)—— P2 / T2-9 切档隔离与进度条(V2-P2 卡 points 4/5)。
3
+ *
4
+ * 权威依据(评审 §3.5 采纳条目 + V2-P2 卡):
5
+ * · **切档 = 强制全量重建 + 进度条**(用户裁定):每次**实际切换**创建**新 rebuild generation**,
6
+ * 目标重建**不复用旧代一二级缓存**(含 e5→BGE→e5);完成后恢复日常增量。
7
+ * · **进度只能有一个真实所有者**:本模块持有唯一进度状态;`semantic-status` 与向导只**投影**它,
8
+ * 不自行维护第二套计数(python-setup 的 status 只读投影,见 index.js semantic-status 接线)。
9
+ * · **保存后才启动切换**:调用方在配置落盘后调 `beginEngineSwitchPre`,不由下拉框未保存值驱动。
10
+ * · **进度只能来自实际完成量**:`done` 由 `reportScopeDone` 逐项累加;**manifest 发布前不得显示整体完成**
11
+ * (`phase` 未到 'ready' 前 `indexReady` 恒 false,即便 done==total 也须等 `publish`)。
12
+ * · **A 未完成又发起 B**:B 使 A 作废(superseded);**A 迟到不能发布 B 的 ready**(switchId 校验)。
13
+ * · **同一 switchId 重试不重复编码**:已完成(ready)的同 id 再次 begin 直接返回现状,不重跑。
14
+ * · **编码中暂停 / 校验失败 / 发布失败 / 重启恢复**:状态可持久化(IO 注入),重启后 running → interrupted,
15
+ * `done` 保留(来自实际完成量),下次 begin 或 resume 继续。
16
+ *
17
+ * 状态字段(最小投影,不泄内容):
18
+ * {switchId, generation, phase, fromIdentity, toIdentity, total, done, failed, error,
19
+ * startedAt, updatedAt, finishedAt, supersededBy, published}
20
+ * phase ∈ idle | running | interrupted | publishing | ready | failed | cancelled
21
+ *
22
+ * 零第三方依赖;IO 全注入(不传则纯内存);UTF-8 无 BOM。
23
+ */
24
+
25
+ export const ENGINE_SWITCH_VERSION = 'engine_switch_v1'
26
+
27
+ /** 终态:到达后不再变化(除 begin 开新代)。 */
28
+ const TERMINAL = new Set(['ready', 'failed', 'cancelled'])
29
+
30
+ function nowOf(opts) { return typeof opts.now === 'function' ? Number(opts.now()) || 0 : Date.now() }
31
+
32
+ /**
33
+ * @param {object} opts
34
+ * @param {()=>number} [opts.now] 时间注入(测试确定性)
35
+ * @param {{readJson?:(p:any)=>any, writeJson?:(p:any,o:any)=>void}} [opts.io] 持久化 IO(缺省纯内存)
36
+ * @param {string} [opts.persistPath] 状态文件路径(提供且 io 可用时才落盘)
37
+ * @param {(msg:string)=>void} [opts.diag]
38
+ */
39
+ export function createEngineSwitchPre(opts = {}) {
40
+ const io = opts.io || null
41
+ const persistPath = opts.persistPath ? String(opts.persistPath) : ''
42
+ const diagFn = typeof opts.diag === 'function' ? opts.diag : () => {}
43
+ const safeDiag = (m) => { try { diagFn(String(m).slice(0, 200)) } catch (_) {} }
44
+
45
+ let generation = 0
46
+ let state = {
47
+ switchId: '', generation: 0, phase: 'idle',
48
+ fromIdentity: '', toIdentity: '', total: 0, done: 0, failed: 0,
49
+ error: '', supersededBy: '', published: false,
50
+ startedAt: 0, updatedAt: 0, finishedAt: 0,
51
+ }
52
+ // 同代内"已处理单元"集合:同 switchId 重试/恢复时跳过(不重复编码)
53
+ let doneUnits = new Set()
54
+
55
+ function snapshot() {
56
+ return {
57
+ ...state,
58
+ doneUnits: doneUnits.size,
59
+ indexReady: state.phase === 'ready' && state.published === true,
60
+ version: ENGINE_SWITCH_VERSION,
61
+ }
62
+ }
63
+
64
+ function persist() {
65
+ if (!persistPath || !io || typeof io.writeJson !== 'function') return
66
+ try {
67
+ io.writeJson(persistPath, {
68
+ version: ENGINE_SWITCH_VERSION,
69
+ generation, state: { ...state },
70
+ doneUnits: [...doneUnits],
71
+ })
72
+ } catch (e) { safeDiag('engine-switch persist fail: ' + ((e && e.message) || e)) }
73
+ }
74
+
75
+ /** 启动恢复:running → interrupted(进度保留 = 实际完成量);ready 保持。 */
76
+ function restore() {
77
+ if (!persistPath || !io || typeof io.readJson !== 'function') return
78
+ let raw = null
79
+ try { raw = io.readJson(persistPath) } catch (_) { return }
80
+ if (!raw || typeof raw !== 'object') return
81
+ try {
82
+ const st = raw.state && typeof raw.state === 'object' ? raw.state : null
83
+ if (!st) return
84
+ generation = Number(raw.generation) || 0
85
+ state = {
86
+ switchId: String(st.switchId || ''), generation: Number(st.generation) || generation,
87
+ phase: String(st.phase || 'idle'),
88
+ fromIdentity: String(st.fromIdentity || ''), toIdentity: String(st.toIdentity || ''),
89
+ total: Number(st.total) || 0, done: Number(st.done) || 0, failed: Number(st.failed) || 0,
90
+ error: String(st.error || ''), supersededBy: String(st.supersededBy || ''),
91
+ published: st.published === true,
92
+ startedAt: Number(st.startedAt) || 0, updatedAt: Number(st.updatedAt) || 0, finishedAt: Number(st.finishedAt) || 0,
93
+ }
94
+ if (Array.isArray(raw.doneUnits)) doneUnits = new Set(raw.doneUnits.map(String))
95
+ // ★T2-9c 重启恢复:**未完成的 running 一律降级为 interrupted**(进程已换代),
96
+ // 进度保持"实际完成量",不假装完成;indexReady 仍为 false(published 未置位)。
97
+ if (state.phase === 'running' || state.phase === 'publishing') {
98
+ state.phase = 'interrupted'
99
+ state.error = state.error || 'interrupted-by-restart'
100
+ state.updatedAt = nowOf(opts)
101
+ persist()
102
+ }
103
+ } catch (e) { safeDiag('engine-switch restore fail: ' + ((e && e.message) || e)) }
104
+ }
105
+ restore()
106
+
107
+ /**
108
+ * 开始一次**实际**引擎切换(保存配置之后调用)。
109
+ * @returns {{ok:boolean, switchId:string, generation:number, reason?:string, resumed?:boolean}}
110
+ * - 同一 switchId 且已 ready → 直接返回现状(**不重复编码**,T2-9b 幂等)。
111
+ * - 同一 switchId 且 running/interrupted → 恢复续跑(done 保留)。
112
+ * - 新 switchId → generation+1,作废旧的在飞切换(supersededBy),清空进度重新计。
113
+ */
114
+ function beginEngineSwitchPre(input = {}) {
115
+ const switchId = String(input.switchId || '').trim()
116
+ const toIdentity = String(input.toIdentity || '').trim()
117
+ const fromIdentity = String(input.fromIdentity || state.toIdentity || '').trim()
118
+ const total = Number(input.total) > 0 ? Math.floor(Number(input.total)) : 0
119
+ if (!switchId) return { ok: false, switchId: '', generation, reason: 'no-switch-id' }
120
+ if (!/^engid_[0-9a-f]{32}$/.test(toIdentity)) return { ok: false, switchId, generation, reason: 'bad-identity' }
121
+ // 幂等:同 id 已完成 → 不重跑
122
+ if (state.switchId === switchId && state.phase === 'ready' && state.toIdentity === toIdentity) {
123
+ return { ok: true, switchId, generation: state.generation, reason: 'already-ready' }
124
+ }
125
+ // 续跑:同 id 未完成 → 保留 done/doneUnits
126
+ if (state.switchId === switchId && (state.phase === 'running' || state.phase === 'interrupted')) {
127
+ state.phase = 'running'
128
+ if (total > 0) state.total = total
129
+ state.updatedAt = nowOf(opts)
130
+ persist()
131
+ return { ok: true, switchId, generation: state.generation, resumed: true }
132
+ }
133
+ // 新切换:作废旧代(A 迟到不得发布 B 的 ready)
134
+ const prevSwitchId = state.switchId
135
+ if (prevSwitchId && !TERMINAL.has(state.phase)) state.supersededBy = switchId
136
+ generation = (Number(state.generation) || generation) + 1
137
+ doneUnits = new Set()
138
+ const t = nowOf(opts)
139
+ state = {
140
+ switchId, generation, phase: 'running',
141
+ fromIdentity, toIdentity, total, done: 0, failed: 0,
142
+ error: '', supersededBy: '', published: false,
143
+ startedAt: t, updatedAt: t, finishedAt: 0,
144
+ }
145
+ persist()
146
+ return { ok: true, switchId, generation, fresh: true, superseded: prevSwitchId || '' }
147
+ }
148
+
149
+ /**
150
+ * 报告一批实际完成的单元(由索引侧在**真实完成编码/同步**后调用)。
151
+ * 幂等:同一 unitKey 重复上报不重复计数(同一 switchId 重试不重复编码)。
152
+ */
153
+ function reportDone(switchId, unitKeys, opts2 = {}) {
154
+ if (state.switchId !== switchId) return { ok: false, reason: 'stale-switch' } // A 迟到不算数
155
+ if (state.phase !== 'running' && state.phase !== 'publishing') return { ok: false, reason: 'not-running' }
156
+ const keys = (Array.isArray(unitKeys) ? unitKeys : [unitKeys]).map(String).filter(Boolean)
157
+ let added = 0
158
+ for (const k of keys) { if (!doneUnits.has(k)) { doneUnits.add(k); added++ } }
159
+ state.done = doneUnits.size
160
+ if (Number(opts2.total) > 0) state.total = Math.floor(Number(opts2.total))
161
+ state.updatedAt = nowOf(opts)
162
+ persist()
163
+ return { ok: true, done: state.done, added }
164
+ }
165
+
166
+ /** 记录失败单元数(不影响 done;T2-9d:失败要让词法仍可用 + 原因可见)。 */
167
+ function reportFailed(switchId, n = 1, reason = '') {
168
+ if (state.switchId !== switchId) return { ok: false, reason: 'stale-switch' }
169
+ state.failed += Math.max(0, Math.floor(Number(n) || 0))
170
+ if (reason) state.error = String(reason).slice(0, 160)
171
+ state.updatedAt = nowOf(opts)
172
+ persist()
173
+ return { ok: true, failed: state.failed }
174
+ }
175
+
176
+ /**
177
+ * 发布 manifest → 进入 ready(**整体完成只能在这里之后显示**)。
178
+ * A 迟到发布:如果当前 state.switchId 不是它,直接拒绝(T2-9b)。
179
+ */
180
+ function publishReady(switchId) {
181
+ if (state.switchId !== switchId) return { ok: false, reason: 'stale-switch' }
182
+ if (state.phase !== 'running' && state.phase !== 'publishing' && state.phase !== 'interrupted') {
183
+ return { ok: false, reason: 'bad-phase' }
184
+ }
185
+ state.phase = 'ready'
186
+ state.published = true
187
+ state.updatedAt = nowOf(opts)
188
+ state.finishedAt = state.updatedAt
189
+ persist()
190
+ return { ok: true, indexReady: true }
191
+ }
192
+
193
+ /** 校验失败 / 发布失败:进入 failed,原因可见(词法侧不受影响)。 */
194
+ function failSwitch(switchId, reason = '') {
195
+ if (state.switchId !== switchId) return { ok: false, reason: 'stale-switch' }
196
+ state.phase = 'failed'
197
+ state.error = String(reason || 'failed').slice(0, 160)
198
+ state.published = false
199
+ state.updatedAt = nowOf(opts)
200
+ state.finishedAt = state.updatedAt
201
+ persist()
202
+ return { ok: true }
203
+ }
204
+
205
+ /** 用户取消(或切档前放弃):进入 cancelled;进度保留在读数里可审计。 */
206
+ function cancelEngineSwitchPre(switchId) {
207
+ const id = String(switchId || state.switchId || '')
208
+ if (!id || state.switchId !== id) return { ok: false, reason: 'stale-switch' }
209
+ if (TERMINAL.has(state.phase) && state.phase !== 'ready') return { ok: false, reason: 'bad-phase' }
210
+ state.phase = 'cancelled'
211
+ state.published = false
212
+ state.updatedAt = nowOf(opts)
213
+ state.finishedAt = state.updatedAt
214
+ persist()
215
+ return { ok: true }
216
+ }
217
+
218
+ /** 计划总量(调用方在拿到冻结快照后设定;只影响进度分母)。 */
219
+ function setTotal(switchId, total) {
220
+ if (state.switchId !== switchId) return { ok: false, reason: 'stale-switch' }
221
+ state.total = Math.max(0, Math.floor(Number(total) || 0))
222
+ if (state.total && state.done > state.total) state.done = doneUnits.size = Math.min(doneUnits.size, state.total)
223
+ state.updatedAt = nowOf(opts)
224
+ persist()
225
+ return { ok: true }
226
+ }
227
+
228
+ function getEngineSwitchStatusPre() { return snapshot() }
229
+
230
+ /** 当前代是否要求"强制全量重建"(running/interrupted 时 true;ready/cancelled/idle 时 false)。 */
231
+ function requiresFullRebuild() { return state.phase === 'running' || state.phase === 'interrupted' }
232
+
233
+ return {
234
+ version: ENGINE_SWITCH_VERSION,
235
+ beginEngineSwitchPre,
236
+ cancelEngineSwitchPre,
237
+ getEngineSwitchStatusPre,
238
+ reportDone,
239
+ reportFailed,
240
+ publishReady,
241
+ failSwitch,
242
+ setTotal,
243
+ requiresFullRebuild,
244
+ _stateForTest: () => state,
245
+ _doneUnitsForTest: () => doneUnits,
246
+ }
247
+ }
@@ -1,3 +1,4 @@
1
+ import { stripRuntimeIntentPre } from './intent-clean-safe.js'
1
2
  /**
2
3
  * M8-1 Episodic Store 纯核心(docs/proactive-associative-memory-system-map.html M-02 Episodic)。
3
4
  * 纯内存状态机,零 IO、零依赖(node:crypto 仅作确定性身份);持久化通过可注入 IO 接口,
@@ -99,10 +100,27 @@ export function createEpisodicStorePre(opts = {}) {
99
100
  const stats = { segmentsAppended: 0, consolidated: 0, droppedTooShort: 0, retained: 0 }
100
101
 
101
102
  function defaultEpisodeId(sessionRef, startedAt) {
102
- const h = createHash('sha256').update(['episode-pre-v1', String(sessionRef), String(startedAt)].join('\u0000')).digest('hex')
103
+ const h = createHash('sha256').update(['episode-v1', String(sessionRef), String(startedAt)].join('\u0000')).digest('hex')
103
104
  return EPISODE_ID_PREFIX + h.slice(0, 32)
104
105
  }
105
106
 
107
+ /**
108
+ * issue#57 修复(2026-09-19):restore 时对 `data.current` 做**形状校验**。
109
+ * 旧实现 `current = data.current || null` 零校验(与 :222 的 validateEpisodePre 形成不对称):
110
+ * 磁盘上 `current:{}`(截断/手改/旧版本残留)会被原样采纳 ⇒ 之后 consolidate() 在
111
+ * `current.segments.length`(:202)抛 TypeError ⇒ **巩固链路静默停摆**,且因异常发生在
112
+ * 调用方 try 之外,统计与日志都不留痕。
113
+ * 纪律:**丢弃优于卡死** —— 形状不合格一律置 null(等效"本会话无未巩固缓冲"),
114
+ * 绝不把结构非法对象放进状态机。
115
+ */
116
+ function restoreCurrentPre(raw) {
117
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
118
+ if (typeof raw.sessionRef !== 'string' || !raw.sessionRef) return null
119
+ if (typeof raw.startedAt !== 'number' || !Number.isFinite(raw.startedAt)) return null
120
+ if (!Array.isArray(raw.segments) || !Array.isArray(raw.userTexts) || !Array.isArray(raw.assistantTexts)) return null
121
+ return raw
122
+ }
123
+
106
124
  // ---- 段追加(会话进行中实时累积) ----
107
125
  function append(seg) {
108
126
  if (disposed) return { ok: false, reason: 'disposed' }
@@ -118,7 +136,8 @@ export function createEpisodicStorePre(opts = {}) {
118
136
  }
119
137
  }
120
138
  // 累积文本(只保留文本特征,不保留原文全文 —— 隐私最小化)
121
- const ut = typeof s.userText === 'string' ? s.userText : ''
139
+ // issue#30:入库时就剥离运行时信封,避免注入快照一路流到 intent/title(下游只做兜底清洗)。
140
+ const ut = stripRuntimeIntentPre(typeof s.userText === 'string' ? s.userText : '')
122
141
  const at = typeof s.assistantText === 'string' ? s.assistantText : ''
123
142
  if (ut.trim()) current.userTexts.push(ut.trim().slice(0, 200))
124
143
  if (at.trim()) current.assistantTexts.push(at.trim().slice(0, 200))
@@ -140,15 +159,14 @@ export function createEpisodicStorePre(opts = {}) {
140
159
  return { ok: true, segments: current.segments.length }
141
160
  }
142
161
 
143
- /** 从累积文本提取 intent(2026-08-28 提纯):跳过本插件注入快照污染的段
144
- * (「Current runtime context. This snapshot…」/auto-memory 标记),取第一条
145
- * 真实用户文本;全被污染则取最后一条(退化)。 */
162
+ /** 从累积文本提取 intent(2026-08-28 提纯;2026-09-16 issue#30 改为共享清洗器)。
163
+ * 旧实现用一条正则猜"像注入"的文本,而注入形态会演进(新增标签/换行拼法)⇒ 漏判后
164
+ * 污染文本进入 intent,再经 hub 变成"观察型 procedure"的 title。现在复用
165
+ * `stripRuntimeIntentPre`(与 intent-clean 同源),按**行边界**剥离运行时信封,
166
+ * 保留代码块/引用块内的字面示例,避免误删真实文本。 */
146
167
  function extractIntent(ep) {
147
- const INJECTED_RE = /^current runtime context\.|^this snapshot supersedes|auto-memory|\[自动沉淀\]/i
148
- const texts = (ep.userTexts || []).map((t) => String(t || '').trim()).filter((t) => t)
149
- const clean = texts.filter((t) => !INJECTED_RE.test(t))
150
- const pick = clean.length ? clean[0] : (texts.length ? texts[texts.length - 1] : '')
151
- return pick ? pick.slice(0, 60) : '(未提取)'
168
+ const clean = (ep.userTexts || []).map((t) => stripRuntimeIntentPre(t).trim()).filter(Boolean)
169
+ return clean.length ? clean[0].slice(0, 60) : ''
152
170
  }
153
171
 
154
172
  /** 从累积文本提取 entities(简单启发式:用户文本中的 CJK 2-4 字符 token,高频优先)。 */
@@ -279,6 +297,39 @@ export function createEpisodicStorePre(opts = {}) {
279
297
  } : null,
280
298
  }
281
299
  }
300
+ /**
301
+ * ★ 增量导入(2026-09-19 上游 PR #77 / issue #63 同步落地,**P0 数据丢失**)。
302
+ *
303
+ * **为什么必须单独有这个函数**:hub 的 `ingestJudgement` 原本对每行 `episodic_candidate`
304
+ * 调 `restore({schemaVersion:1, episodes:[row]})`,而 `restore()` 是**快照整体替换**语义
305
+ * (先 `episodes = []`)。worker 产出的候选行普遍缺 `validateEpisodePre` 必填字段
306
+ * ⇒ 校验必拒(`restored:0`),**但 episodes 已被清空、current 已被置 null**,
307
+ * 且 `restore()` 仍返回 `{ok:true}` ⇒ hub 记 `consumedEpisodic++` / `outcome:'restored'`
308
+ * ⇒ 下次 consolidate/flush 把清空态落盘 ⇒ **一次 ingest 抹掉全部已巩固 episode,不可逆**。
309
+ *
310
+ * 契约(与 `restore` 严格区分):
311
+ * - **绝不清空既有状态**(不清 episodes、不动 current);
312
+ * - 逐条校验,**只追加合法项**,非法项计入 `rejected`(不静默);
313
+ * - 按 `episodeId` **幂等去重**(重复导入同一行不产生副本);
314
+ * - **不持久化**(由调用方决定何时 flush),与 `restore` 一致。
315
+ * @param {Array} rows - 候选 episode 行(原始形态,内部走 validateEpisodePre)
316
+ * @returns {{ok: boolean, imported: number, rejected: number, duplicates: number, reason?: string}}
317
+ */
318
+ function importEpisodes(rows) {
319
+ if (!Array.isArray(rows)) return { ok: false, imported: 0, rejected: 0, duplicates: 0, reason: 'bad-rows' }
320
+ let imported = 0, rejected = 0, duplicates = 0
321
+ const seen = new Set(episodes.map((e) => e.episodeId))
322
+ for (const raw of rows) {
323
+ const v = validateEpisodePre(raw)
324
+ if (!v.ok) { rejected++; continue }
325
+ if (seen.has(v.episode.episodeId)) { duplicates++; continue }
326
+ episodes.push(v.episode)
327
+ seen.add(v.episode.episodeId)
328
+ imported++
329
+ }
330
+ return { ok: true, imported, rejected, duplicates }
331
+ }
332
+
282
333
  function restore(data) {
283
334
  if (!data || data.schemaVersion !== 1) return { ok: false, reason: 'bad-schema' }
284
335
  if (!Array.isArray(data.episodes)) return { ok: false, reason: 'bad-episodes' }
@@ -288,7 +339,7 @@ export function createEpisodicStorePre(opts = {}) {
288
339
  if (!v.ok) continue
289
340
  episodes.push(v.episode)
290
341
  }
291
- current = data.current || null
342
+ current = restoreCurrentPre(data.current)
292
343
  return { ok: true, restored: episodes.length }
293
344
  }
294
345
  function clear() {
@@ -308,7 +359,7 @@ export function createEpisodicStorePre(opts = {}) {
308
359
 
309
360
  return {
310
361
  append, consolidate, flush, query, recent, get, statsFor,
311
- snapshot, restore, clear, dispose,
362
+ snapshot, restore, importEpisodes, clear, dispose,
312
363
  getStats: () => ({ ...stats }),
313
364
  get size() { return episodes.length },
314
365
  get hasCurrent() { return !!current },
@@ -32,12 +32,12 @@ export function canonicalWorkspaceKey(key) {
32
32
 
33
33
  /** §10 隐私投影:sessionId → plugin-local sessionRef(不可逆哈希;跨文件稳定)。 */
34
34
  export function sessionRefOf(sessionId) {
35
- return 'sesr_' + first32(sha256Str('evidence-sesref-pre-v1\u0000' + String(sessionId || '')))
35
+ return 'sesr_' + first32(sha256Str('evidence-sesref-v1\u0000' + String(sessionId || '')))
36
36
  }
37
37
 
38
38
  /** §10 隐私投影:workspace 绝对键 → 稳定 workspaceRef(不落盘任何绝对路径)。 */
39
39
  export function workspaceRefOf(workspaceKey) {
40
- return 'wsr_' + first32(sha256Str('evidence-wsref-pre-v1\u0000' + canonicalWorkspaceKey(workspaceKey)))
40
+ return 'wsr_' + first32(sha256Str('evidence-wsref-v1\u0000' + canonicalWorkspaceKey(workspaceKey)))
41
41
  }
42
42
 
43
43
  /**
@@ -118,7 +118,14 @@ export class EvidenceEventStore {
118
118
  }
119
119
  if (id) this._appended.add(id)
120
120
  this._chain = this._chain.then(() => this._writeLine(proj.line))
121
- return this._chain.then((written) => ({ ok: written, reason: written ? 'ok' : 'write-failed', evidenceId: id, projected: proj.projected }))
121
+ return this._chain.then((written) => {
122
+ // issue#56 修复(2026-09-19):写盘失败必须**释放幂等登记**。
123
+ // 旧实现只 add 从不回退 ⇒ 一次瞬时写失败后,同 evidenceId 的重试恒被判
124
+ // duplicate-evidence 而拒绝 ⇒ 该条证据**静默永久丢失**。
125
+ // 登记保留在调用时(维持同步去重窗口),失败时撤销(允许重试)。
126
+ if (!written && id) this._appended.delete(id)
127
+ return { ok: written, reason: written ? 'ok' : 'write-failed', evidenceId: id, projected: proj.projected }
128
+ })
122
129
  }
123
130
 
124
131
  async _writeLine(line) {
package/lib/fact-store.js CHANGED
@@ -148,7 +148,7 @@ export function createFactStorePre(opts = {}) {
148
148
  // ---- 确定性 factId(与 memoryId/anchorId 同风格) ----
149
149
  function defaultFactId(subject, predicate, object, scope) {
150
150
  const h = createHash('sha256').update(
151
- ['fact-pre-v1', String(scope), String(subject), String(predicate), String(object == null ? '' : object)].join('\u0000')
151
+ ['fact-v1', String(scope), String(subject), String(predicate), String(object == null ? '' : object)].join('\u0000')
152
152
  ).digest('hex')
153
153
  return FACT_ID_PREFIX + h.slice(0, 32)
154
154
  }
@@ -181,10 +181,17 @@ export function createFactStorePre(opts = {}) {
181
181
  // conflictId 必须唯一:同一候选值反复出现时,每次冲突都是独立待决事件。
182
182
  // 用 subject+predicate+object+序号+detectedAt 派生,保证可被逐个 resolve。
183
183
  conflictSeq++
184
+ // ★ 登记**检测时快照**而非活引用(2026-09-19 上游 PR #80 第 3 项 / issue #67 同步落地):
185
+ // 旧实现 `left: existing` 持 store 内活对象引用 ⇒ 后续 merge 会**原地改写** existing.confidence
186
+ // 并对 existing.provenance **数组原地 push** ⇒ 已展示/已落盘(facts.json)的冲突左侧
187
+ // ≠ 检测时的值 ⇒ 审计面失真("当时判定冲突的两个值"被事后改写)。
188
+ // 故此处取快照,provenance 额外做数组副本。
184
189
  conflicts.push({
185
190
  conflictId: defaultFactId(c.scope, c.subject, c.predicate, c.object) + '_conflict_' + conflictSeq + '_' + String(now),
186
191
  scope: c.scope, subject: c.subject, predicate: c.predicate,
187
- left: existing, right: c, detectedAt: now, resolved: false,
192
+ left: { ...existing, provenance: Array.isArray(existing.provenance) ? [...existing.provenance] : existing.provenance },
193
+ right: { ...c, provenance: Array.isArray(c.provenance) ? [...c.provenance] : c.provenance },
194
+ detectedAt: now, resolved: false,
188
195
  })
189
196
  void persist() // 冲突集是重要状态,必须落盘(不持久化会丢失待决冲突)
190
197
  return { ok: true, outcome: 'conflict-added', conflict: conflicts[conflicts.length - 1], existing }
@@ -384,7 +391,12 @@ export function createFactStorePre(opts = {}) {
384
391
  function clear() {
385
392
  facts = []; conflicts = []
386
393
  try { io.clear() } catch (_) {}
387
- stats.created = 0; stats.merged = 0; stats.superseded = 0
394
+ // ★ issue #76-6c 修复(2026-09-19):统计字段**全量归零**。
395
+ // 旧实现只写 `stats.created = 0; stats.merged = 0; stats.superseded = 0`,
396
+ // 漏掉 `upserts / conflictAdded / inferenceBlocked / expiredIgnored / revoked`
397
+ // ⇒ clear() 后这些计数残留(实测),诊断读数与实际不符。
398
+ // 改为**按现有键遍历归零**,将来新增 stats 字段也不会再漏。
399
+ for (const k of Object.keys(stats)) stats[k] = 0
388
400
  return { ok: true }
389
401
  }
390
402
  function dispose(reason) {
@@ -396,6 +408,13 @@ export function createFactStorePre(opts = {}) {
396
408
  return {
397
409
  upsert, supersede, get, query, conflictsList, pendingConflicts, resolveConflict,
398
410
  evidenceFor, revokeBySource, snapshot, restore, clear, dispose,
411
+ // ★ issue #76-5 修复(2026-09-19):把**模块级**的 judgement-row 消费器挂进实例。
412
+ // 旧实现 `memory-hub.js:81-84` 检查 `typeof stores.facts.factCandidateFromJudgementRow === 'function'`
413
+ // 以决定是否委托——但该函数此前**只是模块级导出**(见上方 `export function`),
414
+ // 不在 `createFactStorePre()` 的返回对象上 ⇒ **该分支恒假** ⇒ hub 恒走自己的本地副本
415
+ // `factCandidateFromRow`(丢掉 `ttl` 字段,实测同输入下 store 版 ttl=60000、hub 版无 ttl)
416
+ // ⇒ 两适配器从此各自演化。挂进实例后委托分支变为恒真,语义统一到 store 实现。
417
+ factCandidateFromJudgementRow,
399
418
  getStats: () => ({ ...stats }),
400
419
  get size() { return facts.length },
401
420
  get conflictCount() { return conflicts.length },
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 有界 rename 重试 —— 应对 Windows 下短暂的文件句柄争用(issue #48)。
3
+ *
4
+ * 背景:Windows 上 DSH 常持有会话/记忆文件句柄(子代理刚结束时仍在写),
5
+ * 此时 `fs.rename` 抛 EPERM/EACCES/EBUSY。旧实现是"一次失败即硬失败",
6
+ * 在并发子代理场景下会把本可成功的写入判死。
7
+ *
8
+ * 语义边界:
9
+ * - delays 是**每次尝试之前的等待时长**,不是绝对时间戳;首项必须为 0(立即试一次)。
10
+ * - **不做** unlink/copy 回退:调用方各自保留自己的原子性与失败策略
11
+ * (删源再拷会破坏原子性,一旦中途失败会同时丢源与目标)。
12
+ * - 仅对**瞬时**错误码重试;其它错误(如 ENOENT)立即抛出,不掩盖真实故障。
13
+ * - 用尽重试仍失败 → 抛出最后一次的原始错误(保留 code/path 等信息)。
14
+ */
15
+ import { promises as fsDefault } from 'node:fs'
16
+ import { setTimeout as sleepDefault } from 'node:timers/promises'
17
+
18
+ export const RENAME_RETRY_DELAYS = Object.freeze([0, 50, 150, 400, 1000])
19
+ const TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EACCES', 'EBUSY'])
20
+
21
+ /**
22
+ * 带退避的 rename。
23
+ * @param {string} from 源路径
24
+ * @param {string} to 目标路径
25
+ * @param {{fs?:object, delays?:number[], sleep?:Function}} [opts] 注入点(fs/sleep 供故障注入测试)
26
+ * @returns {Promise<void>} 成功即 resolve;失败抛原始错误
27
+ */
28
+ export async function retryRename(from, to, opts = {}) {
29
+ const fsApi = opts.fs || fsDefault
30
+ const configuredDelays = opts.delays || RENAME_RETRY_DELAYS
31
+ const delays = Array.isArray(configuredDelays) ? configuredDelays.slice() : configuredDelays
32
+ const sleep = opts.sleep || sleepDefault
33
+ if (!Array.isArray(delays) || !delays.length || delays[0] !== 0 ||
34
+ delays.some((ms) => !Number.isFinite(ms) || ms < 0)) {
35
+ throw new TypeError('retryRename: delays must start with 0 and contain finite nonnegative waits')
36
+ }
37
+ for (let attempt = 0; attempt < delays.length; attempt++) {
38
+ if (delays[attempt]) await sleep(delays[attempt])
39
+ try {
40
+ await fsApi.rename(from, to)
41
+ return
42
+ } catch (error) {
43
+ if (!TRANSIENT_RENAME_CODES.has(error && error.code) || attempt === delays.length - 1) throw error
44
+ }
45
+ }
46
+ }
package/lib/index-sync.js CHANGED
@@ -16,7 +16,19 @@ import {
16
16
  M7_INDEX_POLICY_VERSION_V1,
17
17
  } from './m7-wire.js'
18
18
 
19
- export const INDEX_SYNC_PAGE_BUDGET_V1 = Object.freeze({ maxRecordsPerPage: 64, maxPageBytes: 256 * 1024 })
19
+ /**
20
+ * ★ 页字节预算(2026-09-19 上游 PR #78 / issue #64 同步落地)。
21
+ *
22
+ * **为什么是 252KiB 而不是 256KiB**:`maxPageBytes` 只约束**页 payload**,
23
+ * 而 m7-wire 的 `MAX_LINE_BYTES` 是 **256KiB**,约束的是**整条线帧**(payload + 帧信封 + 换行)。
24
+ * 两者同为 256KiB ⇒ payload 落入约 231B 临界窗口的页 **builder 接受、线上必超限**
25
+ * ⇒ worker 判 `line-oversize` fail-closed 退出。
26
+ * 更糟的是叠加了死锁:① 熔断阈值 3,但每段 `index_sync_begin` 的成功帧把计数清零 ⇒ 峰值 2 永不熔断;
27
+ * ② `syncId` 由 recordCount **确定性派生** ⇒ 重试恒重建同一坏页 ⇒ 每 Segment 重生 worker、索引永不 ready。
28
+ *
29
+ * 故留 **4KiB 余量**(≈ 实测信封 202–230B 的 18 倍)。
30
+ */
31
+ export const INDEX_SYNC_PAGE_BUDGET_V1 = Object.freeze({ maxRecordsPerPage: 64, maxPageBytes: 252 * 1024 })
20
32
  const SCOPE_ORDER_PRE = ['Workspace', 'User']
21
33
 
22
34
  function pagePayloadBytes(syncId, pageNo, pageCount, records) {