@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,156 @@
1
+ /**
2
+ * config-io.js —— 配置文件的**原子写**与**损坏隔离**(上游 issue #82 修复)。
3
+ *
4
+ * ## 背景(#82;已在 pre 线实跑核验:两条路径在优化后的版本里**仍然存在**)
5
+ *
6
+ * ① **写侧非原子**:配置落盘是裸 `writeFileSync`(同步路径)/ 裸 `writeFile`(异步路径)。
7
+ * 进程写到一半被杀、或磁盘写满,会留下**半截 JSON**;下次启动 `JSON.parse` 抛错,
8
+ * 于是直接回落出厂默认 ⇒ **用户此前的设置静默丢失**。
9
+ * 同类裸写还出现在 `embedding-config.json`(语义引擎配置)。
10
+ *
11
+ * ② **读侧静默**:两个 load 函数的 catch 一律 `return _mergeConfigPre(null)`(出厂默认),
12
+ * 错误只塞进 `this._readError` —— **用户看不到「我的配置坏了、已被重置」**。
13
+ * 本仓铁律:fail-soft 必须返回**可观察**信号;静默回落等于「数据丢了但没人知道」。
14
+ *
15
+ * ## 本模块只做两件事,且**不改变既有语义**
16
+ *
17
+ * 1. `writeTextAtomicPreSync` / `writeTextAtomicPre`:写 tmp → `renameSync` 覆盖目标。
18
+ * 同目录 rename 在同一文件系统上是**原子**的 ⇒ 读方永远只见「旧的完整版」或
19
+ * 「新的完整版」,**永远不会看到半截文件**。
20
+ * 2. `readJsonQuarantinePreSync`:解析失败时**先把坏文件改名留存**
21
+ * (`<name>.corrupt-<ts>`)再返回结构化失败。这样 ①用户数据没被覆盖
22
+ * ②诊断面能看见「曾经坏过、坏在哪」。
23
+ *
24
+ * ## 纪律
25
+ * - 零运行时依赖:只用 `node:fs` / `node:fs/promises` / `node:path`。
26
+ * - **fail-soft 但必须留痕**:任何一步失败都不抛,返回结构化结果让调用方处理。
27
+ * - CRLF、无 BOM。
28
+ */
29
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
30
+ import { writeFile } from 'node:fs/promises'
31
+ import path from 'node:path'
32
+
33
+ /** 临时文件后缀。与既有 hub 落盘的 `.tmp` 同族,便于排查时一眼认出。 */
34
+ export const ATOMIC_TMP_SUFFIX_V1 = '.tmp'
35
+
36
+ /** 损坏留存的默认后缀前缀。 */
37
+ export const CORRUPT_QUARANTINE_PREFIX_V1 = '.corrupt-'
38
+
39
+ /** 时间戳串(用于损坏留存命名)。绝不影响主流程。 */
40
+ function tsPre(now) {
41
+ try {
42
+ const d = typeof now === 'number' ? new Date(now) : new Date()
43
+ const p = (n) => String(n).padStart(2, '0')
44
+ return String(d.getFullYear()) + p(d.getMonth() + 1) + p(d.getDate())
45
+ + '-' + p(d.getHours()) + p(d.getMinutes()) + p(d.getSeconds())
46
+ } catch (_) {
47
+ return '0'
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 把目标文件挪到同目录的旁路名(**不删除** —— 留证据)。
53
+ * @returns {string} 旁路路径;失败返回空串(fail-soft,绝不抛)。
54
+ */
55
+ export function quarantineFilePreSync(file, opts = {}) {
56
+ try {
57
+ const suffix = (opts && opts.suffix) || (CORRUPT_QUARANTINE_PREFIX_V1 + tsPre(opts && opts.now))
58
+ const dest = file + suffix
59
+ // 同一秒内坏两次 ⇒ 再加一段随机后缀,避免覆盖上一份证据
60
+ const finalPath = existsSync(dest) ? dest + '.' + Math.random().toString(36).slice(2, 6) : dest
61
+ renameSync(file, finalPath)
62
+ return finalPath
63
+ } catch (_) {
64
+ return ''
65
+ }
66
+ }
67
+
68
+ /**
69
+ * **同步原子写**:tmp → rename。
70
+ * @returns {{ok: boolean, path?: string, error?: string}} 结构化结果(fail-soft,不抛)。
71
+ */
72
+ export function writeTextAtomicPreSync(file, text) {
73
+ const tmp = file + ATOMIC_TMP_SUFFIX_V1
74
+ try {
75
+ mkdirSync(path.dirname(file), { recursive: true })
76
+ writeFileSync(tmp, String(text), 'utf8')
77
+ renameSync(tmp, file)
78
+ return { ok: true, path: file }
79
+ } catch (e) {
80
+ // 失败时清掉临时文件,避免在目标目录留垃圾(清理失败也不抛)
81
+ try { rmSync(tmp, { force: true }) } catch (_) {}
82
+ return { ok: false, error: String((e && e.message) || e) }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * **异步原子写**:tmp → rename。语义与同步版一致。
88
+ *
89
+ * 只把**可能较大**的正文写入交给异步 IO;rename 本身极快,保持同步调用。
90
+ * 这样与既有 `persistConfigPre` 的异步口径对齐,不引入额外的 await 链。
91
+ */
92
+ export async function writeTextAtomicPre(file, text) {
93
+ const tmp = file + ATOMIC_TMP_SUFFIX_V1
94
+ try {
95
+ mkdirSync(path.dirname(file), { recursive: true })
96
+ await writeFile(tmp, String(text), 'utf8')
97
+ renameSync(tmp, file)
98
+ return { ok: true, path: file }
99
+ } catch (e) {
100
+ try { rmSync(tmp, { force: true }) } catch (_) {}
101
+ return { ok: false, error: String((e && e.message) || e) }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * **读 JSON + 损坏隔离**(同步)。
107
+ *
108
+ * 行为矩阵:
109
+ * - 文件不存在 ⇒ `{ ok:false, missing:true, reason:'ENOENT' }`(不算损坏,不留存)
110
+ * - 解析/读取失败 ⇒ **先留存**坏文件,再 `{ ok:false, corrupted:true, quarantined, reason }`
111
+ * - 正常 ⇒ `{ ok:true, value }`
112
+ *
113
+ * ⚠️ 与旧行为唯一的差别:**解析失败时多了一次 rename**(把原文件挪走)。
114
+ * 之所以必须挪走而非原地不动,是因为调用方随后会回落出厂默认并**覆盖写回** ——
115
+ * 不挪走的话坏文件当场被新配置盖掉,用户数据无从取证。
116
+ */
117
+ export function readJsonQuarantinePreSync(file, opts = {}) {
118
+ let raw
119
+ try {
120
+ raw = readFileSync(file, 'utf8')
121
+ } catch (e) {
122
+ if (e && e.code === 'ENOENT') return { ok: false, missing: true, reason: 'ENOENT' }
123
+ return { ok: false, corrupted: false, reason: String((e && e.message) || e) }
124
+ }
125
+ try {
126
+ return { ok: true, value: JSON.parse(raw) }
127
+ } catch (e) {
128
+ const reason = String((e && e.message) || e)
129
+ const quarantined = quarantineFilePreSync(file, opts)
130
+ return { ok: false, corrupted: true, quarantined, reason }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 纯函数判据:一段文本是否**看起来像被截断的 JSON**。
136
+ *
137
+ * 用途:调用方先自行 readFileSync 再 parse、拿不到本模块的返回对象时,
138
+ * 可用它给出更准确的诊断文案(「疑似写入被中断」而不是笼统的 parse 失败)。
139
+ */
140
+ export function looksTruncatedJsonPre(text) {
141
+ const s = String(text == null ? '' : text).trim()
142
+ if (!s) return false
143
+ if (s[0] !== '{' && s[0] !== '[') return false
144
+ try { JSON.parse(s); return false } catch (_) { return true }
145
+ }
146
+
147
+ /** 是否是一个**非空**普通文件(区分「损坏但非空」与「空文件」用)。 */
148
+ export function isNonEmptyFilePreSync(file) {
149
+ try {
150
+ const st = statSync(file)
151
+ return st.isFile() && st.size > 0
152
+ } catch (_) {
153
+ return false
154
+ }
155
+ }
156
+
@@ -157,7 +157,7 @@ export function validateAccessEvidencePre(ev) {
157
157
 
158
158
  /** §4 canonical identity:observationId 由 sessionId+contextVersion+trigger digest+policy 决定。 */
159
159
  export function buildObservationId(sessionId, contextVersion, triggerDigest, policyVersion = CONTEXT_BRIDGE_POLICY_VERSION) {
160
- const parts = ['context-push-pre-v1', String(sessionId || ''), Number(contextVersion) || 0, String(triggerDigest || ''), policyVersion]
160
+ const parts = ['context-push-v1', String(sessionId || ''), Number(contextVersion) || 0, String(triggerDigest || ''), policyVersion]
161
161
  return OBSERVATION_PREFIX + first32(sha256Str(JSON.stringify(parts)))
162
162
  }
163
163
 
@@ -388,7 +388,7 @@ export function createFakeContextSinkPre(opts = {}) {
388
388
  frames.push(JSON.parse(JSON.stringify(frame)))
389
389
  if (frames.length > capacity) frames.shift()
390
390
  stats.accepted++
391
- return { observationId: id, accepted: true, workerEpoch: 'fake-epoch-pre-v1', reason: 'ok' }
391
+ return { observationId: id, accepted: true, workerEpoch: 'fake-epoch-v1', reason: 'ok' }
392
392
  },
393
393
  async closeSession() { stats.closedSessions++ },
394
394
  async dispose() { frames.length = 0 },
@@ -519,6 +519,9 @@ export class BoundedIdSet {
519
519
  constructor(capacity) { this.capacity = Math.max(1, Number(capacity) || 256); this._set = new Set() }
520
520
  has(id) { return this._set.has(id) }
521
521
  add(id) { this._set.add(id); if (this._set.size > this.capacity) { const first = this._set.values().next().value; this._set.delete(first) } }
522
+ /** issue#56(2026-09-19):显式撤销登记。用于「先登记后写盘」的幂等缓存——
523
+ * 写盘失败时必须释放 id,否则该 id 被永久判为重复 ⇒ 瞬时失败后重试恒被拒(证据静默丢失)。 */
524
+ delete(id) { return this._set.delete(id) }
522
525
  get size() { return this._set.size }
523
526
  clear() { this._set.clear() }
524
527
  }
@@ -15,6 +15,7 @@
15
15
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
16
16
  import { createHash } from 'node:crypto'
17
17
  import path from 'node:path'
18
+ import { resolveDshHomeForEnginePre, resolveDshHomePre } from './dsh-home.js'
18
19
  import {
19
20
  buildContextPushEnvelopePre, buildAuthorizedMemoryRefFromRecord, createAccessEvidencePre,
20
21
  createCiteEvidencesFromText, createCorrectionEvidencesFromText, computeReadCoverage,
@@ -37,11 +38,14 @@ const dropLoggedSkip = new Map()
37
38
  /** M7.5 诊断:ctx-host drop/skip 直写 harness diagnose 日志(此前 try{diag()} 静默吞 ReferenceError)。 */
38
39
  function diagCtx(msg) {
39
40
  try {
40
- const env = process.env.DSH_HOME
41
- const base = env && env.trim() ? env.trim()
42
- : (process.env.USERPROFILE || process.env.HOME || '')
41
+ // ★#86-3:统一口径。原实现把 `USERPROFILE || HOME` **直接**当 DSH_HOME(不拼 .dsh)
42
+ // ⇒ 在 Windows 上得到 `C:\Users\X` 而非 `C:\Users\X\.dsh`,与其余 6 处**指向不同目录**。
43
+ // ★同时修掉一个**潜伏 bug**:原代码在 `DSH_HOME` 已设置时仍 `path.join(base, '.dsh', ...)`,
44
+ // 即落成 `$DSH_HOME/.dsh/dsh-auto-memory-diagnose.log`(**多拼一层 .dsh**);
45
+ // 未设置时才恰好正确。resolveDshHomePre() 返回的**已经是 dsh home 本身**,故不再拼 .dsh。
46
+ const base = resolveDshHomePre()
43
47
  if (!base) return
44
- appendFileSync(path.join(base, '.dsh', 'dsh-auto-memory-diagnose.log'),
48
+ appendFileSync(path.join(base, 'dsh-auto-memory-diagnose.log'),
45
49
  new Date().toISOString() + ' [ctx-host] ' + String(msg).slice(0, 300) + '\n', 'utf8')
46
50
  } catch (e) {}
47
51
  }
@@ -104,6 +108,12 @@ export function createContextHost(opts = {}) {
104
108
  let lastFrameIdentity = null // 最小投影:observationId 全值(本身是哈希)+计数,无任何文本
105
109
  let lastSegmentRuntimeKey = null
106
110
  let lastSegmentSessionRef = null
111
+ // C5/I7:最近一次"索引未就绪"降级(可读、可观测;含原因与时间,不含任何语料文本)
112
+ // ★多工作区适配(2026-09-17):改为**按会话分片**。旧实现是单变量,任何会话的 index-not-ready
113
+ // 都写进去,而读取方(`index.js` 的 buildTierLayerInjection)只判 10 分钟时间窗、**不判会话**
114
+ // ⇒ B 会话的"未就绪"会把 A 会话污染十分钟,表现为**跨会话假降级**(A 明明正常却被标注降级)。
115
+ const indexDegradeBySession = new Map()
116
+ let lastIndexDegrade = null // 兼容投影:最近一次(供 debugView 等旧读取方)
107
117
  let lastSinkMode = null
108
118
  let bridge = null
109
119
  let store = null
@@ -111,10 +121,8 @@ export function createContextHost(opts = {}) {
111
121
  const registry = new CorpusRegistry({ sidecarDir: path.join(dshHome(), 'memory', 'index', 'files') })
112
122
 
113
123
  function dshHome() {
114
- const env = process.env.DSH_HOME
115
- if (env && env.trim()) return env.trim()
116
- const base = engine.__homedirFn ? engine.__homedirFn() : (process.env.USERPROFILE || process.env.HOME || '')
117
- return base ? path.join(base, '.dsh') : '.'
124
+ // ★#86-3:统一口径(含 __homedirFn 注入点保留)
125
+ return resolveDshHomeForEnginePre(engine)
118
126
  }
119
127
  function effectiveEnabled() {
120
128
  return engine.config.associativeMemoryEnabled === true && engine.config.contextBridgeEnabled === true
@@ -347,14 +355,33 @@ export function createContextHost(opts = {}) {
347
355
  ? Promise.resolve().then(() => engine._jsSemanticRank(corpusSnap, buildObserveWindowText(runtime, seg))).catch(() => null)
348
356
  : Promise.resolve(null)
349
357
  void Promise.all([readyPromise, c2RankPromise]).then(([readyRes, rank]) => {
358
+ // C5/I7(2026-09-14):索引未就绪**不再静默丢弃注入**。旧行为是 pushDrop('index-not-ready:…')
359
+ // 后直接 return —— 语义索引一卡死,注入面就整段消失且无人知(07:43–08:35 的违规现场)。
360
+ // 新行为 = 显式降级 + fail-open:记一条可读降级(reason 进注入层 `[降级]` 行),本轮按**词法命中**
361
+ // 继续组装并推送 envelope(Tier-0 目录层照常常驻),稠密臂本轮不参与融合(不装懂)。
362
+ let indexNotReady = ''
350
363
  if (!(readyRes && readyRes.ready)) {
351
- stats.pushesRejected++
352
- pushDrop('index-not-ready:' + ((readyRes && readyRes.reason) || 'unknown'), runtime.contextVersion, runtime.key)
353
- return
364
+ indexNotReady = String((readyRes && readyRes.reason) || 'unknown')
365
+ stats.indexDegraded = (stats.indexDegraded || 0) + 1
366
+ const sidDeg = String(runtime.sessionId || '')
367
+ const degradeRec = { reason: indexNotReady, at: Date.now(), sessionId: sidDeg }
368
+ // 多工作区适配(2026-09-17):按会话分片写入,读取方按会话取 ⇒ 不再跨会话污染。
369
+ // ★2026-09-20 移植(issue #94⑤ / PR #100):先 delete 再 set 刷新插入序。
370
+ indexDegradeBySession.delete(sidDeg)
371
+ indexDegradeBySession.set(sidDeg, degradeRec)
372
+ if (indexDegradeBySession.size > 32) {
373
+ const oldest = indexDegradeBySession.keys().next().value
374
+ indexDegradeBySession.delete(oldest)
375
+ }
376
+ lastIndexDegrade = degradeRec // 兼容投影(最近一次)
377
+ try { engine._lastIndexDegrade = degradeRec } catch (_) {}
378
+ try { engine._indexDegradeBySession = indexDegradeBySession } catch (_) {}
379
+ pushDrop('index-not-ready:degraded:' + indexNotReady, runtime.contextVersion, runtime.key)
380
+ diagCtx('ctx-host degrade: index-not-ready(' + indexNotReady + ') → 词法命中 + 常驻目录层继续注入')
354
381
  }
355
382
  if (st.disposed || runtime.disposed) return
356
383
  let keptList = ls.kept
357
- if (rank && rank.scores && rank.scores.size) {
384
+ if (!indexNotReady && rank && rank.scores && rank.scores.size) {
358
385
  // D6 融合候选池=词法 kept ∪ 稠密 top-K(语料中找回记录对象;语义强命中但词法零分者由此入选)
359
386
  const poolMap = new Map()
360
387
  for (const k of ls.kept) if (k && k.memoryId) poolMap.set(k.memoryId, k)
@@ -419,7 +446,10 @@ export function createContextHost(opts = {}) {
419
446
  // 2026-08-27 判定观测:按段类型计数(真实数据验证 CoT 触发)
420
447
  stats.jsDecideRuns++
421
448
  stats.jsDecideByKind[seg.kind] = (stats.jsDecideByKind[seg.kind] || 0) + 1
422
- const cd = Math.max(0, Number(engine.config.jsDecideCooldownRounds) || 5)
449
+ // ★2026-09-20 移植(issue #94④ / PR #100):`|| 5` 把用户显式配置的 0(不冷却)反转成 5。
450
+ // 仅缺失/非有限数才回落默认值。
451
+ const cdRaw = Number(engine.config.jsDecideCooldownRounds)
452
+ const cd = Number.isFinite(cdRaw) && cdRaw >= 0 ? cdRaw : 5
423
453
  const stNow = stateFor(runtime)
424
454
  if (cd > 0 && stNow._jsDecideNextAt && Date.now() < stNow._jsDecideNextAt) return
425
455
  var jsDecideQueryText = buildObserveWindowText(runtime, seg)
@@ -463,14 +493,21 @@ export function createContextHost(opts = {}) {
463
493
  const session = built.frame.session || {}
464
494
  const cursor = built.frame.cursor || {}
465
495
  const obs = built.frame.observationId || ''
496
+ // 2026-09-14 P1-⑮:注入侧 reason 串补 0-1 分值(intent/dense/margin)。
497
+ // 数值段置于 reasonCodes **之前**,160 字符截断只砍代码列表、不砍分值。
498
+ const num2 = (x) => (Number.isFinite(Number(x)) ? Number(x).toFixed(2) : '0.00')
499
+ const jsReason = ('js-decide lane=' + dec.lane + ' ' + (dec.decision || 'emit') +
500
+ ' intent=' + num2(dec.features && dec.features.intentProb) +
501
+ ' dense=' + num2(dec._denseTop) + ' margin=' + num2(dec._margin) +
502
+ ' ' + (dec.reasonCodes || []).join(',')).slice(0, 160)
466
503
  const act = {
467
504
  schemaVersion: 1, namespace: 'dsh-auto-memory', kind: 'activation_request',
468
505
  activationId: 'act_' + (obs.startsWith('obs_') ? obs.slice(8, 40) : 'jsdecide'),
469
- observationId: obs, workerEpoch: 'js-decide-pre-v1',
506
+ observationId: obs, workerEpoch: 'js-decide-v1',
470
507
  sessionId: session.sessionId || '', agentId: session.agentId || '',
471
508
  workspaceKey: session.workspaceKey || '', scope: session.scope || 'Workspace',
472
509
  contextVersion: cursor.contextVersion || 0, memoryIndexVersion: built.frame.index ? built.frame.index.memoryIndexVersion : '',
473
- threshold: { policyVersion: dec.features ? 'activation_policy_v2' : 'activation_policy_v2', score: (dec.features && dec.features.intentProb) || 0, threshold: 0.45, reason: ('js-decide lane=' + dec.lane + ' ' + (dec.reasonCodes || []).join(',')) },
510
+ threshold: { policyVersion: dec.features ? 'activation_policy_v2' : 'activation_policy_v2', score: (dec.features && dec.features.intentProb) || 0, threshold: 0.45, reason: jsReason },
474
511
  level: 'excerpt',
475
512
  candidates: (function () {
476
513
  // 2026-08-27 优化③:候选方案档位(balanced=3×40 / dense=6×20 / custom=自定义)
@@ -532,7 +569,39 @@ export function createContextHost(opts = {}) {
532
569
  }
533
570
  }
534
571
  } catch (_) {}
572
+ // C5 闸门:记下本轮 query(激活请求本身不带 query 文本),供注入层判"是问为什么还是只要索引"
573
+ try {
574
+ const qSid = String((act && act.sessionId) || '')
575
+ const qRec = {
576
+ at: Date.now(),
577
+ sessionId: qSid,
578
+ text: String(jsDecideQueryText || '').slice(0, 1200),
579
+ }
580
+ // 多工作区适配(2026-09-17):按会话分片。旧实现单槽无条件覆盖 ⇒ A 的 query 被 B 顶掉,
581
+ // 读取侧因会话不匹配而退回 triggerText(语义降级)。分片后各会话各记一份。
582
+ if (!engine._lastTierQueryBySession || typeof engine._lastTierQueryBySession.set !== 'function') {
583
+ engine._lastTierQueryBySession = new Map()
584
+ }
585
+ engine._lastTierQueryBySession.set(qSid, qRec)
586
+ if (engine._lastTierQueryBySession.size > 32) {
587
+ const oldest = engine._lastTierQueryBySession.keys().next().value
588
+ engine._lastTierQueryBySession.delete(oldest)
589
+ }
590
+ engine._lastTierQuery = qRec // 兼容投影(最近一次)
591
+ } catch (_) {}
535
592
  engine._activationHost.offerExternalActivation(act)
593
+ // 命中投影补上 query(记录发生在 offer 内部;此处补字段,顺序确定)
594
+ // 多工作区适配(2026-09-17):分片条目与兼容投影**都要补**,否则按会话取到的投影缺 question。
595
+ try {
596
+ const actSid = String((act && act.sessionId) || '')
597
+ const qText = String(jsDecideQueryText || '').slice(0, 1200)
598
+ const rec = engine._tierGateHitsBySession && typeof engine._tierGateHitsBySession.get === 'function'
599
+ ? engine._tierGateHitsBySession.get(actSid) : null
600
+ if (rec && String(rec.sessionId) === actSid) rec.question = qText
601
+ if (engine._tierGateHits && String(engine._tierGateHits.sessionId) === actSid) {
602
+ engine._tierGateHits.question = qText
603
+ }
604
+ } catch (_) {}
536
605
  }).catch(() => {})
537
606
  } catch (_) {}
538
607
  }
@@ -723,6 +792,8 @@ export function createContextHost(opts = {}) {
723
792
  durableEventsOnDisk: evidenceRootExists() ? storeFor().loadEvents().events.length : 0,
724
793
  stats: { ...stats, bridge: { ...b.stats } },
725
794
  storeStats: store ? { ...store.stats } : null,
795
+ // C5/I7:索引未就绪降级(latest) —— 供面板/注入层解释"为什么这轮只有词法命中"
796
+ indexDegrade: lastIndexDegrade,
726
797
  recentDrops: volatileDrops.slice(-4),
727
798
  }
728
799
  }