@a9i5k4/dsh-auto-memory 3.0.0 → 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 (90) hide show
  1. package/README.md +19 -7
  2. package/README.zh-CN.md +19 -7
  3. package/docs/FRONTEND-CO-CREATION.md +191 -0
  4. package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
  5. package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
  6. package/docs/PROMO-PROMPT-3.0.md +100 -0
  7. package/docs/USER-GUIDE.en.md +2 -2
  8. package/docs/USER-GUIDE.zh-CN.md +2 -2
  9. package/docs/WHITEPAPER.md +207 -0
  10. package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +397 -0
  11. package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +351 -0
  12. package/docs/internal/ART-DIRECTION-WIREFRAME.md +191 -181
  13. package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +181 -0
  14. package/docs/internal/BATTLE-PLAN-20260917.md +871 -0
  15. package/docs/internal/FEATURE-INVENTORY.md +531 -0
  16. package/docs/internal/G-SERIES-EXECUTION-20260917.md +248 -0
  17. package/docs/internal/G3-DESIGN-20260918.md +82 -0
  18. package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +92 -0
  19. package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +309 -0
  20. package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +120 -0
  21. package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +74 -0
  22. package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +175 -0
  23. package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +389 -0
  24. package/docs/internal/ISSUE10-PLAN-20260919.md +254 -0
  25. package/docs/internal/ISSUE10B-FORENSICS-20260919.md +468 -0
  26. package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +150 -0
  27. package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +114 -0
  28. package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +79 -0
  29. package/docs/internal/MEMORY-GOVERNANCE-20260917.md +309 -0
  30. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +705 -0
  31. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +649 -0
  32. package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +225 -0
  33. package/docs/internal/PROGRESS-20260917.md +93 -0
  34. package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +128 -0
  35. package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +163 -0
  36. package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +127 -0
  37. package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +140 -0
  38. package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +138 -0
  39. package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +218 -0
  40. package/docs/internal/RESUME-20260918.md +171 -0
  41. package/docs/internal/RESUME-20260919.md +104 -0
  42. package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +198 -0
  43. package/docs/internal/ROADMAP-20260917-WEEK.md +134 -0
  44. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +13 -3
  45. package/docs/internal/S10-GAP-INVENTORY-20260917.md +239 -0
  46. package/docs/internal/T6-EXECUTION-20260920.md +130 -0
  47. package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +146 -0
  48. package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +89 -0
  49. package/docs/internal/THESIS-OUTLINE-20260918.md +147 -0
  50. package/docs/internal/THREE-LAYER-CONTRACT.md +10 -1
  51. package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +297 -0
  52. package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +104 -0
  53. package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
  54. package/lib/activation-host.js +63 -9
  55. package/lib/board-mode.js +1 -1
  56. package/lib/client.js +892 -27
  57. package/lib/config-io.js +156 -0
  58. package/lib/context-bridge.js +3 -0
  59. package/lib/context-host.js +16 -9
  60. package/lib/degrade.js +385 -0
  61. package/lib/dsh-home.js +143 -0
  62. package/lib/episodic-store.js +52 -2
  63. package/lib/evidence-store.js +8 -1
  64. package/lib/fact-store.js +21 -2
  65. package/lib/index-sync.js +13 -1
  66. package/lib/index.js +1507 -158
  67. package/lib/intent-clean-safe.js +258 -40
  68. package/lib/l0-extract.js +231 -16
  69. package/lib/m4-corpus.js +8 -2
  70. package/lib/m7-index-sync-host.js +8 -1
  71. package/lib/memory-envelope.js +6 -1
  72. package/lib/memory-hub.js +127 -12
  73. package/lib/memory-index.js +4 -2
  74. package/lib/note-status-apply.js +118 -0
  75. package/lib/note-status.js +196 -0
  76. package/lib/procedure-store.js +84 -3
  77. package/lib/python-sidecar-client.js +29 -3
  78. package/lib/recall-fusion.js +83 -12
  79. package/lib/rules-edit.js +159 -0
  80. package/lib/semantic-decide.js +41 -8
  81. package/lib/semantic-js.js +51 -6
  82. package/lib/shadow-host.js +3 -5
  83. package/lib/skill-export-host.js +153 -0
  84. package/lib/skill-export.js +239 -0
  85. package/lib/storage-manage.js +6 -0
  86. package/lib/temporal-parse.js +191 -159
  87. package/lib/tier0-catalog.js +45 -3
  88. package/lib/wb-contract.js +198 -2
  89. package/lib/wb-sidecar.js +54 -3
  90. package/package.json +1 -1
@@ -0,0 +1,143 @@
1
+ /**
2
+ * dsh-home.js —— **DSH_HOME 的唯一解析口径**(上游 issue #86-3 修复)。
3
+ *
4
+ * ## 背景(#86-3;已在 pre 线实跑核验)
5
+ *
6
+ * 修复前,全仓有 **7 处独立解析** `process.env.DSH_HOME`,口径互不相同:
7
+ *
8
+ * | 位置 | 环境变量缺失时的回落 |
9
+ * |---|---|
10
+ * | index.js:808(`dshHome()`) | `path.join(homedir(), '.dsh')` |
11
+ * | index.js:9256(模型根) | `path.join(homedir(), '.dsh')` |
12
+ * | index.js:9613(python-setup) | `path.join(homedir(), '.dsh')`,失败退 `homedir()` |
13
+ * | index.js:9623(python-sidecar) | `path.join(homedir(), '.dsh')`,失败退 **空串** |
14
+ * | semantic-js.js:73/176 | `path.join(homedir(), '.dsh')` |
15
+ * | activation-host.js:72 | 退 `homedir()` 再拼 `.dsh`,全失败退 **'.'** |
16
+ * | context-host.js:40 | 退 **`USERPROFILE || HOME`** 再拼(**前缀不同**) |
17
+ * | shadow-host.js:129 | 同 activation(但注释说漏拼过 `.dsh`) |
18
+ *
19
+ * ⇒ 后果:**同一台机器上,不同子系统可能把数据写到不同根目录**。
20
+ * 最典型的是 `context-host` 用 `USERPROFILE` 作基准,而其余用 `os.homedir()`——
21
+ * 两者在 Windows 上通常一致,但在容器/CI/被改过环境变量的进程里会分叉。
22
+ *
23
+ * ## 本模块的职责
24
+ *
25
+ * 提供**一个**函数 `resolveDshHomePre(override)`,所有站点都调它。
26
+ * 解析顺序(逐级回落,**绝不抛**):
27
+ *
28
+ * 1. `override`(显式传入,最高优先 —— 给测试注入与 engine 级配置留口)
29
+ * 2. `process.env.DSH_HOME`(trim 后非空)
30
+ * 3. `os.homedir()` + `/.dsh`
31
+ * 4. 环境变量 `USERPROFILE || HOME` + `/.dsh`(**保留 context-host 原有的兜底能力**,
32
+ * 只是把它从「基准」降级为「最后兜底」,从而与其余站点统一)
33
+ * 5. 全失败 ⇒ `'.dsh'`(相对路径,保证**永不返回空串**)
34
+ *
35
+ * ## 为什么把 `homedir()` 放在 `USERPROFILE` 之前
36
+ *
37
+ * `os.homedir()` 在 Windows 上**本身就是** `USERPROFILE`(Node 内部优先读它,
38
+ * 读不到才退 `HOMEDRIVE+HOMEPATH`)⇒ 两者绝大多数情况等价,
39
+ * 但 `homedir()` 还会正确处理 `HOME` 覆盖与权限异常 ⇒ **以它为准更稳**。
40
+ * 保留 `USERPROFILE||HOME` 仅作 `homedir()` 抛异常时的兜底。
41
+ *
42
+ * ## 纪律
43
+ * - 零运行时依赖(只 `node:os` / `node:path`)。
44
+ * - **永不抛、永不返回空串**(调用方大量直接 `path.join(dshHome(), ...)`)。
45
+ * - 只读环境变量,**不缓存**(测试会中途改 `process.env.DSH_HOME`)。
46
+ * - CRLF、无 BOM。
47
+ */
48
+ import os from 'node:os'
49
+ import path from 'node:path'
50
+
51
+ /** 环境变量名(集中一处,便于将来改名)。 */
52
+ export const DSH_HOME_ENV_V1 = 'DSH_HOME'
53
+
54
+ /** 默认子目录名。 */
55
+ export const DSH_HOME_DIRNAME_V1 = '.dsh'
56
+
57
+ /** 全失败时的最后兜底(相对路径,保证返回非空)。 */
58
+ export const DSH_HOME_FALLBACK_V1 = '.dsh'
59
+
60
+ /**
61
+ * 取 home 基准目录(用于拼 `.dsh`)。**永不抛**。
62
+ * @returns {string} 非空字符串,或空串(表示取不到基准)
63
+ */
64
+ function homeBasePre() {
65
+ // ① os.homedir() —— 首选:Windows 上等价于 USERPROFILE,且能处理 HOME 覆盖
66
+ try {
67
+ const h = os.homedir()
68
+ if (h && String(h).trim()) return String(h).trim()
69
+ } catch (_) {
70
+ // 落到 ②
71
+ }
72
+ // ② USERPROFILE / HOME —— 兼容 homedir() 抛异常的极端环境
73
+ try {
74
+ const e = process.env.USERPROFILE || process.env.HOME || ''
75
+ if (e && String(e).trim()) return String(e).trim()
76
+ } catch (_) {}
77
+ return ''
78
+ }
79
+
80
+ /**
81
+ * **唯一入口**:解析 DSH_HOME。
82
+ *
83
+ * @param {string} [override] 显式覆盖(测试注入 / engine 级配置);空串视为未提供。
84
+ * @returns {string} 非空路径字符串(**永不抛、永不返回空串**)
85
+ */
86
+ export function resolveDshHomePre(override) {
87
+ // ① 显式覆盖优先
88
+ try {
89
+ if (override != null && String(override).trim()) return String(override).trim()
90
+ } catch (_) {}
91
+ // ② 环境变量
92
+ try {
93
+ const env = process.env[DSH_HOME_ENV_V1]
94
+ if (env && String(env).trim()) return String(env).trim()
95
+ } catch (_) {}
96
+ // ③ / ④ 基准目录 + .dsh
97
+ const base = homeBasePre()
98
+ if (base) {
99
+ try {
100
+ return path.join(base, DSH_HOME_DIRNAME_V1)
101
+ } catch (_) {}
102
+ }
103
+ // ⑤ 最后兜底
104
+ return DSH_HOME_FALLBACK_V1
105
+ }
106
+
107
+ /**
108
+ * engine 级便捷包装:优先用 `engine.__dshHomeOverride`,其次环境变量,最后默认。
109
+ *
110
+ * 之所以要这一层:`activation-host` / `context-host` / `shadow-host` 都是
111
+ * 「engine + 可选 __homedirFn」的形态,统一改调本函数可让三者的口径完全一致,
112
+ * 同时**保留** `__homedirFn` 这个既有测试注入点(不再各自手写回落链)。
113
+ */
114
+ export function resolveDshHomeForEnginePre(engine) {
115
+ const e = engine || {}
116
+ // ★★ 优先级必须与**原实现**一致:`env` 优先于 `__homedirFn`。
117
+ // 原写法是 `const env = process.env.DSH_HOME; if (env.trim()) return env.trim();
118
+ // const base = engine.__homedirFn ? ... : ...` ⇒ env 先判。
119
+ // ⚠️ 2026-09-20 首次实现把 __homedirFn 提到 env 之前,导致用 `process.env.DSH_HOME`
120
+ // 注入的测试(如 smoke-test-m53)被真实 homedir 覆盖,证据写到了**真实用户目录**
121
+ // (症状:C4/C5/C6 evidence 落盘数为 0,离真因很远)。
122
+ // ① 显式 engine 级覆盖(新增能力,原实现没有,放最前不影响兼容)
123
+ try {
124
+ if (e.__dshHomeOverride != null && String(e.__dshHomeOverride).trim()) {
125
+ return String(e.__dshHomeOverride).trim()
126
+ }
127
+ } catch (_) {}
128
+ // ② 环境变量(与原实现同优先级)
129
+ try {
130
+ const env = process.env[DSH_HOME_ENV_V1]
131
+ if (env && String(env).trim()) return String(env).trim()
132
+ } catch (_) {}
133
+ // ③ 既有注入点 __homedirFn:返回的是「home 基准目录」,仍需拼 .dsh
134
+ try {
135
+ if (typeof e.__homedirFn === 'function') {
136
+ const base = e.__homedirFn()
137
+ if (base && String(base).trim()) return path.join(String(base).trim(), DSH_HOME_DIRNAME_V1)
138
+ }
139
+ } catch (_) {}
140
+ // ④ 默认链(homedir → USERPROFILE/HOME → '.dsh')
141
+ return resolveDshHomePre()
142
+ }
143
+
@@ -104,6 +104,23 @@ export function createEpisodicStorePre(opts = {}) {
104
104
  return EPISODE_ID_PREFIX + h.slice(0, 32)
105
105
  }
106
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
+
107
124
  // ---- 段追加(会话进行中实时累积) ----
108
125
  function append(seg) {
109
126
  if (disposed) return { ok: false, reason: 'disposed' }
@@ -280,6 +297,39 @@ export function createEpisodicStorePre(opts = {}) {
280
297
  } : null,
281
298
  }
282
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
+
283
333
  function restore(data) {
284
334
  if (!data || data.schemaVersion !== 1) return { ok: false, reason: 'bad-schema' }
285
335
  if (!Array.isArray(data.episodes)) return { ok: false, reason: 'bad-episodes' }
@@ -289,7 +339,7 @@ export function createEpisodicStorePre(opts = {}) {
289
339
  if (!v.ok) continue
290
340
  episodes.push(v.episode)
291
341
  }
292
- current = data.current || null
342
+ current = restoreCurrentPre(data.current)
293
343
  return { ok: true, restored: episodes.length }
294
344
  }
295
345
  function clear() {
@@ -309,7 +359,7 @@ export function createEpisodicStorePre(opts = {}) {
309
359
 
310
360
  return {
311
361
  append, consolidate, flush, query, recent, get, statsFor,
312
- snapshot, restore, clear, dispose,
362
+ snapshot, restore, importEpisodes, clear, dispose,
313
363
  getStats: () => ({ ...stats }),
314
364
  get size() { return episodes.length },
315
365
  get hasCurrent() { return !!current },
@@ -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
@@ -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 },
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) {