@furongjun1999/dsh-memory 0.4.8 → 0.4.10

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 (232) hide show
  1. package/README.md +54 -26
  2. package/codebuddy/CODEBUDDY.md +196 -195
  3. package/codebuddy/README.md +13 -1
  4. package/codebuddy/mcp.json +9 -0
  5. package/docs/GBrain/345/217/257/345/200/237/351/211/264/347/202/271_/347/201/265/346/236/242/350/220/275/347/202/271/344/272/244/346/216/245_20260919.md +169 -0
  6. package/docs/README.md +1 -1
  7. package/docs/discipline/harnesses.yaml +18 -7
  8. package/docs/discipline/templates/full.md.tmpl +4 -3
  9. package/docs/experiments/linkref_backfill/candidates_20260917.json +726 -0
  10. package/docs/experiments/linkref_backfill/candidates_internal_20260917.json +602 -0
  11. package/docs/experiments/linkref_backfill/candidates_internal_v2.json +603 -0
  12. package/docs/experiments/linkref_backfill/candidates_secret_20260917.json +884 -0
  13. package/docs/experiments/linkref_backfill/candidates_secret_v2.json +789 -0
  14. package/docs/hive//345/244/232/347/253/257harness/351/200/232/344/277/241/345/245/221/347/272/246_v0.1.md +42 -0
  15. package/docs/hive//346/243/200/347/264/242/346/224/266/346/225/233/345/256/236/346/265/213/344/270/216S1b/350/256/276/350/256/241_v0.1.md +43 -0
  16. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +102 -0
  17. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S1b/344/270/216/345/217/254/345/233/236/346/235/203/350/241/241_v0.1.md +57 -0
  18. package/docs/hive//347/234/237/345/256/236/345/272/223/347/253/257/345/210/260/347/253/257/345/256/236/346/265/213_S7/345/200/222/346/216/222/345/200/231/351/200/211/345/261/202_v0.1.md +126 -0
  19. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +503 -0
  20. package/docs/mdcg/D_meta_/345/267/245/347/250/213/345/214/226/346/226/271/346/241/210_v0.2.md +216 -0
  21. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.5.md +631 -625
  22. package/docs/mdcg//344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/345/245/221/347/272/246_v0.1.md +82 -0
  23. package/docs/mdcg//345/205/250/345/272/223/344/273/243/347/240/201/350/257/204/345/256/241/344/270/216/346/235/241/344/273/266/345/214/226/346/263/250/351/207/212_/350/256/241/345/210/222_v0.1.md +600 -0
  24. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +47 -45
  25. package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/{206_v0.4.md → 206_v0.5.md} +92 -4
  26. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +172 -0
  27. package/docs/mdcg//347/216/257/344/272/214_/347/231/275/347/256/261/345/241/253/345/205/205/346/265/201/346/260/264/347/272/277_v0.1.md +31 -0
  28. package/docs/mdcg//350/267/250/347/253/257/351/252/214/350/257/201/344/270/216/345/220/214/346/255/245/345/215/217/350/256/256_v0.1.md +93 -0
  29. package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +2 -2
  30. package/docs/theory//347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
  31. package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +2 -2
  32. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +434 -433
  33. package/docs//347/201/265/346/236/242/350/207/252/346/210/221/346/224/271/350/277/233/345/267/245/344/275/234/350/256/241/345/210/222_/345/244/226/351/203/250/347/240/224/347/251/266/347/263/273/345/210/227/345/220/270/346/224/266_v1_20260919.md +286 -0
  34. package/dsh/README.md +33 -0
  35. package/dsh/cordis.yml.example +13 -7
  36. package/dsh/hive-mcp-probe.mjs +94 -0
  37. package/dsh/hive-mcp.example.yml +62 -0
  38. package/dsh/update-lingshu.bat +11 -0
  39. package/dsh/update-lingshu.ps1 +337 -0
  40. package/lib/hooks.d.ts +3 -0
  41. package/lib/hooks.js +17 -23
  42. package/lib/index.d.ts +4 -2
  43. package/lib/index.js +29 -6
  44. package/lib/lib/datapath.d.ts +76 -1
  45. package/lib/lib/datapath.js +199 -13
  46. package/lib/lib/mdcg_client.d.ts +40 -3
  47. package/lib/lib/mdcg_client.js +46 -22
  48. package/lib/lib/mutual.js +4 -4
  49. package/lib/lib/token_store.js +4 -5
  50. package/md_cg/audit.py +17 -2
  51. package/md_cg/autonomy.py +86 -15
  52. package/md_cg/backfill.py +36 -1
  53. package/md_cg/backfill_bigdomain.py +34 -0
  54. package/md_cg/bench6_arms.py +28 -1
  55. package/md_cg/bench6_common.py +10 -1
  56. package/md_cg/bench6_competitors.py +6 -1
  57. package/md_cg/bench_axis_domain.py +9 -1
  58. package/md_cg/bench_blind_comp.py +7 -1
  59. package/md_cg/bench_en_atoms_public.py +9 -0
  60. package/md_cg/bench_governance.py +348 -0
  61. package/md_cg/bench_lme_zh.py +16 -1
  62. package/md_cg/bench_locomo.py +2 -1
  63. package/md_cg/bench_locomo_zh.py +16 -1
  64. package/md_cg/bench_locomo_zh_public.py +4 -1
  65. package/md_cg/bench_longmem.py +2 -1
  66. package/md_cg/bench_membench.py +27 -1
  67. package/md_cg/bench_p0.py +4 -1
  68. package/md_cg/bench_progressive.py +13 -1
  69. package/md_cg/bench_role_views.py +238 -0
  70. package/md_cg/bench_task_ab.py +8 -1
  71. package/md_cg/bench_task_ab_llm.py +13 -1
  72. package/md_cg/bench_unified_en.py +6 -1
  73. package/md_cg/bench_zh_mad.py +20 -1
  74. package/md_cg/blindspot_tickets.py +123 -0
  75. package/md_cg/branches.py +12 -1
  76. package/md_cg/build_postings.py +73 -0
  77. package/md_cg/ccgc.py +67 -2
  78. package/md_cg/census.py +5 -1
  79. package/md_cg/chain.py +24 -3
  80. package/md_cg/codeindex.py +134 -17
  81. package/md_cg/coldverify.py +265 -0
  82. package/md_cg/comment_gate.py +338 -0
  83. package/md_cg/cond_compose.py +190 -0
  84. package/md_cg/cond_facts.py +155 -0
  85. package/md_cg/cond_template.json +107 -0
  86. package/md_cg/condition_anchor.py +143 -0
  87. package/md_cg/conformance.py +69 -4
  88. package/md_cg/consistency.py +24 -1
  89. package/md_cg/consolidate.py +53 -2
  90. package/md_cg/corpus.py +4 -0
  91. package/md_cg/crosscheck.py +42 -2
  92. package/md_cg/crypto.py +35 -1
  93. package/md_cg/d_meta.py +310 -0
  94. package/md_cg/datapath.py +201 -26
  95. package/md_cg/docindex.py +122 -1
  96. package/md_cg/eval_common.py +29 -1
  97. package/md_cg/evidence.py +27 -1
  98. package/md_cg/evolution.py +21 -1
  99. package/md_cg/export.py +11 -1
  100. package/md_cg/forgetting.py +23 -1
  101. package/md_cg/fsutil.py +18 -1
  102. package/md_cg/hotcache.py +214 -0
  103. package/md_cg/hyperedge.py +251 -0
  104. package/md_cg/identity.py +18 -1
  105. package/md_cg/insight.py +17 -1
  106. package/md_cg/lexicon/build_cedict_en_zh.py +9 -0
  107. package/md_cg/lexicon/build_standard_en.py +171 -168
  108. package/md_cg/lexicon/expand_en_zh.py +6 -0
  109. package/md_cg/lifecycle.py +12 -1
  110. package/md_cg/linkref.py +281 -0
  111. package/md_cg/links.py +29 -1
  112. package/md_cg/mcp_server.py +362 -43
  113. package/md_cg/md_whitebox.py +53 -1
  114. package/md_cg/mdcg.py +1003 -27
  115. package/md_cg/mdcos.py +558 -36
  116. package/md_cg/metacognition.py +37 -2
  117. package/md_cg/migrate.py +4 -0
  118. package/md_cg/migrate_aeis.py +221 -213
  119. package/md_cg/migrate_roleplay.py +8 -0
  120. package/md_cg/migrate_wisdom_graph.py +14 -1
  121. package/md_cg/mreview/__main__.py +3 -0
  122. package/md_cg/mreview/bundle.py +8 -0
  123. package/md_cg/mreview/candidates.py +9 -0
  124. package/md_cg/mreview/govern.py +21 -1
  125. package/md_cg/mreview/locate.py +34 -0
  126. package/md_cg/mreview/pipeline.py +29 -1
  127. package/md_cg/mreview/ruleset.py +16 -1
  128. package/md_cg/nodefile.py +233 -3
  129. package/md_cg/pooling.py +23 -1
  130. package/md_cg/postings.py +298 -0
  131. package/md_cg/predict.py +89 -9
  132. package/md_cg/progressive.py +3 -0
  133. package/md_cg/protect.py +14 -1
  134. package/md_cg/protocol.py +372 -0
  135. package/md_cg/provenance.py +262 -0
  136. package/md_cg/reach.py +453 -0
  137. package/md_cg/refindex.py +47 -2
  138. package/md_cg/refine.py +20 -1
  139. package/md_cg/roleviews.py +89 -0
  140. package/md_cg/routing.py +76 -0
  141. package/md_cg/scrub.py +63 -2
  142. package/md_cg/security.py +26 -1
  143. package/md_cg/self_state.py +64 -1
  144. package/md_cg/selfreport.py +151 -0
  145. package/md_cg/semantic/canonical.py +5 -0
  146. package/md_cg/semantic/en_normalizer.py +364 -355
  147. package/md_cg/semantic/zh_en_atoms.py +139 -136
  148. package/md_cg/signer.py +41 -1
  149. package/md_cg/sources.py +583 -547
  150. package/md_cg/statushdr.py +179 -0
  151. package/md_cg/stg.py +59 -18
  152. package/md_cg/subgraph.py +23 -0
  153. package/md_cg/sustain.py +56 -1
  154. package/md_cg/tasks.py +26 -2
  155. package/md_cg/test_autonomy.py +26 -0
  156. package/md_cg/test_bench_governance.py +102 -0
  157. package/md_cg/test_blindspot_tickets.py +166 -0
  158. package/md_cg/test_ccgc.py +10 -0
  159. package/md_cg/test_codeindex.py +338 -0
  160. package/md_cg/test_comment_gate.py +187 -0
  161. package/md_cg/test_cond_compose_anchors.py +76 -0
  162. package/md_cg/test_condition_anchor.py +82 -0
  163. package/md_cg/test_d_meta.py +412 -0
  164. package/md_cg/test_datapath_root.py +188 -0
  165. package/md_cg/test_gain_gate.py +47 -1
  166. package/md_cg/test_hot_cold.py +187 -0
  167. package/md_cg/test_hyperedge.py +245 -0
  168. package/md_cg/test_linkref.py +306 -0
  169. package/md_cg/test_md_access_parity.py +15 -3
  170. package/md_cg/test_mr_m1.py +108 -18
  171. package/md_cg/test_mr_m3.py +8 -1
  172. package/md_cg/test_p26_refindex.py +49 -20
  173. package/md_cg/test_p27_docindex.py +236 -2
  174. package/md_cg/test_p2_mcp.py +1 -1
  175. package/md_cg/test_p31_insight.py +24 -0
  176. package/md_cg/test_p44_md_whitebox.py +14 -1
  177. package/md_cg/test_protocol.py +243 -0
  178. package/md_cg/test_reach.py +378 -0
  179. package/md_cg/test_reach_keys.py +201 -0
  180. package/md_cg/test_reach_meta_exits.py +145 -0
  181. package/md_cg/test_read_clip.py +8 -4
  182. package/md_cg/test_retr_s1.py +340 -0
  183. package/md_cg/test_retr_s1b.py +209 -0
  184. package/md_cg/test_retr_s3.py +194 -0
  185. package/md_cg/test_retr_s4.py +163 -0
  186. package/md_cg/test_retr_s5.py +200 -0
  187. package/md_cg/test_retr_s6.py +157 -0
  188. package/md_cg/test_retr_s7.py +385 -0
  189. package/md_cg/test_retr_s8_time.py +316 -0
  190. package/md_cg/test_retr_s9_edges.py +286 -0
  191. package/md_cg/test_retr_s9_entity_ctx.py +175 -0
  192. package/md_cg/test_review_conformance.py +59 -2
  193. package/md_cg/test_role_views.py +354 -0
  194. package/md_cg/test_subproc_encoding.py +188 -0
  195. package/md_cg/test_trust.py +361 -0
  196. package/md_cg/test_units_poll.py +71 -0
  197. package/md_cg/test_v14_fixes.py +397 -0
  198. package/md_cg/test_validity_filter.py +280 -0
  199. package/md_cg/test_wisdom_md_store.py +7 -3
  200. package/md_cg/test_writepipe.py +5 -1
  201. package/md_cg/theory.py +16 -1
  202. package/md_cg/tokens.py +40 -8
  203. package/md_cg/tool_face.py +13 -2
  204. package/md_cg/trust.py +943 -0
  205. package/md_cg/twophase.py +12 -1
  206. package/md_cg/units.py +132 -10
  207. package/md_cg/vision_evidence.py +24 -1
  208. package/md_cg/weights.py +24 -1
  209. package/md_cg/whitebox.py +32 -1
  210. package/md_cg/whitebox_kb/data/verify_cache.json +21210 -365
  211. package/md_cg/whitebox_kb/data/verify_savings.jsonl +5078 -0
  212. package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
  213. package/md_cg/whitebox_kb/wisdom/code_compose.py +113 -6
  214. package/md_cg/whitebox_kb/wisdom/code_solidified.json +1 -1
  215. package/md_cg/whitebox_kb/wisdom/verifier.py +340 -55
  216. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db +0 -0
  217. package/md_cg/writelimit.py +18 -4
  218. package/md_cg/writepipe.py +178 -7
  219. package/package.json +2 -2
  220. package/skills/skills/designer-perspective/scripts/__pycache__/designer.cpython-310.pyc +0 -0
  221. package/skills/skills/designer-perspective/scripts/designer.py +17 -1
  222. package/skills/skills/designer-perspective/tests/selftest.py +3 -1
  223. package/src/hooks.ts +17 -21
  224. package/src/index.ts +33 -6
  225. package/src/lib/datapath.ts +211 -13
  226. package/src/lib/mdcg_client.ts +64 -25
  227. package/src/lib/mutual.ts +411 -411
  228. package/src/lib/token_store.ts +4 -5
  229. package/zcode/AGENTS.md +196 -195
  230. package/zcode/README.md +4 -0
  231. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
  232. package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
@@ -40,11 +40,12 @@ write 的六道闸(audit 校验 / consistency 冲突 / review 审核 / gated
40
40
 
41
41
  import time
42
42
 
43
- from . import twophase
43
+ from . import twophase, trust
44
44
 
45
45
  __all__ = ["WritePipeline", "default_pipeline"]
46
46
 
47
47
 
48
+ # 生效条件:调用即对 cg 执行 flush()(无脏数据时为 no-op),且仅当该调用抛异常而 out 是 dict 时在 out 写入 "flush_error",异常本身不外抛;
48
49
  def _commit_visibility(cg, out):
49
50
  """写提交边界(2026-09-16):把内存脏索引落分片日志,使本次写入对其他进程立即可见。
50
51
 
@@ -70,12 +71,14 @@ def _commit_visibility(cg, out):
70
71
  class WritePipeline:
71
72
  """写入拦截器链(实例级;default_pipeline() 提供进程级默认单例)。"""
72
73
 
74
+ # 生效条件:无前置;初始化 _before / _after 两条空链(元素为 (name, fn) 二元组),不做任何注册、不触盘;
73
75
  def __init__(self):
74
76
  self._before = [] # [(name, fn)]
75
77
  self._after = [] # [(name, fn)]
76
78
 
77
79
  # ---------- 注册表 ----------
78
80
 
81
+ # 生效条件:fn 可调用时先按 name 摘除同名项,再在 position 为 None 时把 (str(name), fn) 追加到链尾、否则插入 max(0, int(position))(position=0 非 None,走插入分支);fn 不可调用则抛 TypeError;
79
82
  def register_before(self, name, fn, position=None):
80
83
  """注册 before 拦截器(同名幂等替换;position=None 追加到链尾)。
81
84
 
@@ -90,11 +93,13 @@ class WritePipeline:
90
93
  else:
91
94
  self._before.insert(max(0, int(position)), item)
92
95
 
96
+ # 生效条件:按 str(name) 过滤 _before,仅保留 x[0] != str(name) 的项(即删除全部同名项),并返回删除前后长度是否不等以表示是否确有移除;
93
97
  def unregister_before(self, name):
94
98
  n0 = len(self._before)
95
99
  self._before = [x for x in self._before if x[0] != str(name)]
96
100
  return len(self._before) != n0
97
101
 
102
+ # 生效条件:fn 可调用时先按 name 摘除同名项,再把 (str(name), fn) 追加到 _after 链尾;fn 不可调用则抛 TypeError;
98
103
  def register_after(self, name, fn):
99
104
  """注册 after 观察者:fn(ctx, out),落盘成功后按序调用。"""
100
105
  if not callable(fn):
@@ -102,17 +107,20 @@ class WritePipeline:
102
107
  self.unregister_after(name)
103
108
  self._after.append((str(name), fn))
104
109
 
110
+ # 生效条件:按 str(name) 过滤 _after,仅保留 x[0] != str(name) 的项(即删除全部同名项),并返回删除前后长度是否不等以表示是否确有移除;
105
111
  def unregister_after(self, name):
106
112
  n0 = len(self._after)
107
113
  self._after = [x for x in self._after if x[0] != str(name)]
108
114
  return len(self._after) != n0
109
115
 
116
+ # 生效条件:无前置;返回 {"before": [...注册名], "after": [...注册名]},只暴露名字不暴露函数对象,顺序即执行顺序;
110
117
  def names(self):
111
118
  return {"before": [n for n, _f in self._before],
112
119
  "after": [n for n, _f in self._after]}
113
120
 
114
121
  # ---------- 执行 ----------
115
122
 
123
+ # 生效条件:传入 cg 与 a(a 为假值如 None 时按 {} 处理,nid 取 a.get("node_id") 或其假值回落 "mem_"+毫秒时间戳),任一 before 钩子返回非 None 即记 halted_by 并经 _commit_visibility 短路返回该响应,全部放行则记 twophase 意图后跑 _executor(其抛 BaseException 时记 STATUS_ERROR 并原样重抛)再顺序跑 after 链、_commit_visibility 并返回落盘 out;
116
124
  def execute(self, cg, a):
117
125
  """写入请求入口:跑 before 链 → 链尾执行器 → after 链。
118
126
 
@@ -157,16 +165,23 @@ class WritePipeline:
157
165
  # 默认链(原 mcp_server._cg_dispatch op=="write" 分支,行为逐字节搬运)
158
166
  # --------------------------------------------------------------------------
159
167
 
168
+ # 生效条件:ctx["a"] 经 audit.audit 得出的 state 为 ACCEPT 时返 None 放行,为 REJECT 时经 cg.add_rejected 返回 ok=False/moved_to="rejected",其余 state 经 cg.propose 返回 moved_to="review_queue"(pr 带 dedup 时再附 dedup/dup_of/dup_status 并改写 hint);
160
169
  def _gate_audit(ctx):
161
170
  """校验闸:audit.audit 四态。ACCEPT 放行;REJECT 负记忆;其余入审核队列。"""
162
171
  a = ctx["a"]
163
172
  cg = ctx["cg"]
164
173
  from . import audit
174
+ payload = {"content": a.get("content", ""), "action": a.get("action"),
175
+ "sensitivity": a.get("sensitivity"),
176
+ "topic": a.get("query") or a.get("intent")}
177
+ if (a.get("content_kind") or "").strip() == "hyperedge":
178
+ # 超边验证器(回放比对)需要锚与结构键:fm 键平铺在 a 顶层,
179
+ # 经 hyperedge.audit_payload 装配三键载荷(缺锚由验证器 fail-closed)。
180
+ from . import hyperedge as _he
181
+ payload = _he.audit_payload(a)
165
182
  verdict = audit.audit(
166
183
  (a.get("content_kind") or "").strip(),
167
- {"content": a.get("content", ""), "action": a.get("action"),
168
- "sensitivity": a.get("sensitivity"),
169
- "topic": a.get("query") or a.get("intent")},
184
+ payload,
170
185
  {"cg": cg, "principal": getattr(cg, "principal", None)})
171
186
  ctx["verdict"] = verdict
172
187
  st = verdict["state"]
@@ -207,6 +222,7 @@ def _gate_audit(ctx):
207
222
  return out
208
223
 
209
224
 
225
+ # 生效条件:ctx["a"]["consistency"] 为假值时返回 None;on_conflict 缺键或假值回落 "defer",仅当 verdict=REJECT 且 on_conflict=reject(返回 moved_to="conflict_rejected")或 verdict∈{REJECT,BLINDSPOT} 且 on_conflict=defer(转 review_queue,去重命中时改写 hint)才拦截,其余 on_conflict 取值返回 None;
210
226
  def _gate_consistency(ctx):
211
227
  """冲突闸:节点间自动冲突检测(三级决策)。
212
228
 
@@ -264,6 +280,7 @@ def _gate_consistency(ctx):
264
280
  return out
265
281
 
266
282
 
283
+ # 生效条件:ctx["a"] 的 gated 为假值时返 None 放行;为真值时按 cg.remember_gated 返回的 verdict 落两段式账,且仅 verdict 为 ACCEPT 时 ok/committed 为 True,verdict 为 MERGE 时记 committed 并置 moved_to="merged_into:"+merged_into,verdict 为 DROP/DEFER 时记 aborted 且 moved_to 为其小写值;
267
284
  def _gate_gated(ctx):
268
285
  """主动遗忘闸(gated=true 时启用):writelimit 限流 + forgetting 三问四态。
269
286
 
@@ -315,9 +332,126 @@ def _gate_gated(ctx):
315
332
  out["moved_to"] = "merged_into:" + str(res.get("merged_into"))
316
333
  elif v in ("DROP", "DEFER"):
317
334
  out["moved_to"] = v.lower()
335
+ # gated 是**替代落盘路径**(自行落盘/合并后直接返回终态、不跑 after 链),故
336
+ # 一跳同步传播须在此单独触发——否则 MERGE 类覆写会漏传下游(非对称边界,
337
+ # 与 `_after_trust` 注释互指)。
338
+ if committed or v == "MERGE":
339
+ prop = trust.mark_dependents(
340
+ cg, ctx["nid"],
341
+ reason="上游节点被 gated 写入/合并(内容或验证态可能已变)",
342
+ actor="writepipe:gated", trigger="write_gated")
343
+ if isinstance(prop, dict) and prop.get("changed"):
344
+ out["propagation"] = {"changed": prop.get("changed"),
345
+ "updated": (prop.get("updated") or [])[:10]}
318
346
  return out
319
347
 
320
348
 
349
+ # 生效条件:ctx["a"]["content"] 的「# 子功能:」行含显式跨节点引用(`@<节点 id>`)且 depends_on 解析为空时返回 ok=False/error="E050" 的终态;depends_on 含库中不存在的 id 时返回 ok=False/error="E051" 的终态;其余(无该行 / 哨兵 / 自然语言自述 / 声明且目标齐备)返回 None 放行;
350
+ def _gate_deps(ctx):
351
+ """依赖声明闸(before 链:linkref 之后、audit 之前):**硬拒条件缺失**。
352
+
353
+ 「声明」的界定(收窄裁定 b,2026-09-19):以 `@<节点 id>` 显式引用为界——
354
+ 自然语言**自述子功能**(描述本单元**内部**构成)不构成依赖声明。原因:CCG
355
+ 编译产物六要素必含「子功能」行,若沿用「非哨兵即声明」,每个 CCG 节点落库后
356
+ 都会被自己的闸门永久要求 depends_on(E050 死锁,test_ccgc V16f 实证)。
357
+ 依赖不是必填元数据;**只有显式声称依赖却不落字段**才是违规(声称与落盘不一致)。
358
+
359
+ 为何是硬拒而非告警:依赖是失效传播的**唯一入口**。声明缺失时,「上游变了
360
+ 下游要存疑」这条链从源头就不存在——它既不报错、也不留任何信号,缺陷以
361
+ 「静默不传播」的形态长期存活(比报错更难发现)。故按契约缺失处理,与
362
+ `ccgc` 的 E 码体系同构(E050 声明缺失 / E051 目标不存在)。
363
+
364
+ 张力消解(与 provenance「写路径永不阻断」纪律的边界,二者不冲突):
365
+ - **声明缺失 / 目标不可解析 = 契约违规** → 硬拒(本闸只做这件事);
366
+ - **传播落盘失败 = 运维降级** → 告警不阻断(见 `_after_trust`)。
367
+
368
+ 判据基于**入参**而非落盘后回读:首次写入时节点尚不存在,回读式校验会
369
+ 永远放行(等于闸门失效)。
370
+ """
371
+ a = ctx["a"]
372
+ cg = ctx["cg"]
373
+ from . import nodefile
374
+ if not nodefile.declares_dependency(a.get("content") or ""):
375
+ return None
376
+ deps = trust.as_deps(a.get("depends_on"))
377
+ if not deps:
378
+ return {"ok": False, "id": ctx["nid"], "committed": False,
379
+ "gate": "deps", "error": "E050",
380
+ "verdict": ctx.get("verdict"),
381
+ "hint": ("依赖声明缺失(E050):正文以 " + nodefile.DEP_REF_MARK
382
+ + "<节点 id> 显式声明了跨节点依赖(「# 子功能:」行),"
383
+ "但 depends_on 未给出可解析目标。依赖必须是**可解析的字段**"
384
+ "(形如 depends_on=[\"<被依赖节点 id>\"]),不能只是散文——"
385
+ "否则被依赖单元变动时,下游无处可传。补齐后重试;"
386
+ "若该行只是描述本单元内部构成(自述),去掉 "
387
+ + nodefile.DEP_REF_MARK + " 引用或改填「无」即可。"
388
+ "本闸是契约闸门的正常行为,不是工具故障。")}
389
+ known = set((getattr(cg, "index", None) or {}).get("nodes") or {})
390
+ missing = [d for d in deps if d not in known]
391
+ if missing:
392
+ return {"ok": False, "id": ctx["nid"], "committed": False,
393
+ "gate": "deps", "error": "E051", "missing": missing[:10],
394
+ "verdict": ctx.get("verdict"),
395
+ "hint": "依赖目标不存在(E051):depends_on 指向 "
396
+ + ", ".join(missing[:5])
397
+ + ",但库中查无此节点——声称依赖一个并不存在的地基,"
398
+ "失效传播会在此处断链。请先建立被依赖节点,或修正 id。"}
399
+ return None
400
+
401
+
402
+ # 生效条件:out 为 dict 且 out["committed"] 为真时,调 trust.mark_dependents 做一跳同步传播(异常吞掉并降级),并在节点时间轴非「时效内」时往 out 写 "validity" 提示;其余情况直接返回不做任何动作;
403
+ def _after_trust(ctx, out):
404
+ """after 观察者:落盘后触发**一跳同步传播** + 时效提示。
405
+
406
+ 为何落在 after 而非 before:只有真正落盘(内容确实变了)才构成「地基动了」;
407
+ before 链短路路径(REJECT / DEFER)本就不跑 after 链,语义天然正确。
408
+ 例外:`gated` 是替代落盘路径(自行落盘并返回终态、不跑 after),故它的
409
+ 传播在 `_gate_gated` 内部单独触发(见该处注释)。
410
+
411
+ **永不抛**:传播失败只降级(`trust.mark_dependents` 内部已兜底并写台账),
412
+ 绝不把「写入已成功」改写为失败——与 `_commit_visibility` 同款边界。
413
+ """
414
+ if not isinstance(out, dict) or not out.get("committed"):
415
+ return
416
+ cg = ctx["cg"]
417
+ nid = ctx.get("nid")
418
+ if not nid:
419
+ return
420
+ prop = trust.mark_dependents(
421
+ cg, nid, reason="上游节点被写入/覆写(内容或验证态可能已变)",
422
+ actor="writepipe:trust", trigger="write")
423
+ if isinstance(prop, dict) and prop.get("changed"):
424
+ out["propagation"] = {"changed": prop.get("changed"),
425
+ "updated": (prop.get("updated") or [])[:10]}
426
+ # 热路径失效(2026-09-19 热温冷分层):写入后清缓存
427
+ try:
428
+ from . import hotcache as _hc
429
+ _hc.invalidate(cg, nid)
430
+ except Exception: # noqa: BLE001
431
+ pass # 缓存失效失败不阻断写入(永不抛)
432
+
433
+ # 冷路径入队(2026-09-19 热温冷分层):异步深度验证
434
+ try:
435
+ from . import coldverify as _cv
436
+ _cv.enqueue(cg, nid, action="reverify",
437
+ reason="writepipe:after_trust")
438
+ except Exception: # noqa: BLE001
439
+ pass # 入队失败不阻断写入
440
+
441
+
442
+ # 生效条件:a 的 content_kind 非 hyperedge 即返回 {},为 hyperedge 时延迟导入
443
+ # hyperedge.EXTRA_FM_KEYS 收集 a 中非 None 的对应键返回(其余写入零键透传);
444
+ def _hyperedge_extra(a):
445
+ """hyperedge 写入的 fm 扩展键透传。计划决策 2:超边写入走 write 动词既有
446
+ 审核链、不造新通道——链尾执行器是唯一落盘点,cg.add 经 **extra 落 fm,
447
+ 故扩展键在此透传;落盘面丢字段 = 上游声明静默失效。延迟导入防循环依赖。"""
448
+ if (a.get("content_kind") or "").strip() != "hyperedge":
449
+ return {}
450
+ from . import hyperedge as _he
451
+ return {k: a[k] for k in _he.EXTRA_FM_KEYS if a.get(k) is not None}
452
+
453
+
454
+ # 生效条件:由链尾以含 cg 与 a 的 ctx 调用即无条件执行 cg.add 落盘并返回 ok=True/committed=True,ctx["cvd"] 非 None 时附加 consistency 字段;
321
455
  def _executor(ctx):
322
456
  """链尾执行器(常驻不可卸载):cg.add 直写落盘。
323
457
 
@@ -335,7 +469,13 @@ def _executor(ctx):
335
469
  non_applicable_conditions=a.get("non_applicable_conditions"),
336
470
  override=bool(a.get("override")), consistency=False,
337
471
  derived_from=_split_ids(a.get("derived_from")),
338
- relation=a.get("relation"))
472
+ relation=a.get("relation"),
473
+ # 可验证记忆单元(裁定 D):依赖/双时间轴/验证态随写入落 fm。
474
+ # 透传而非丢弃——落盘面丢字段=上游声明静默失效(比报错难发现)。
475
+ depends_on=trust.as_deps(a.get("depends_on")),
476
+ valid_from=a.get("valid_from"), valid_until=a.get("valid_until"),
477
+ verification_state=a.get("verification_state"),
478
+ **_hyperedge_extra(a))
339
479
  out = {"ok": True, "id": ctx["nid"], "committed": True,
340
480
  "verdict": ctx["verdict"]}
341
481
  if ctx.get("cvd") is not None:
@@ -343,6 +483,7 @@ def _executor(ctx):
343
483
  return out
344
484
 
345
485
 
486
+ # 生效条件:value 传入即无条件延迟导入并转调 mcp_server._split_ids 后原样返回其结果(本符号无自身分支);
346
487
  def _split_ids(value):
347
488
  # 与 mcp_server._split_ids 同源(延迟导入,单一真源)
348
489
  from .mcp_server import _split_ids as _f
@@ -356,17 +497,47 @@ def _split_ids(value):
356
497
  _DEFAULT = None
357
498
 
358
499
 
500
+ # 生效条件:pipe 传入即对其依次注册 before 的 linkref(position=0)/deps/audit/consistency/gated 与 after 的 linkref/trust(同名幂等替换),并返回同一 pipe;
359
501
  def install_default_gates(pipe):
360
- """把默认六道闸以拦截器形态注册(幂等:同名替换,可重复调用)。"""
502
+ """把默认闸以拦截器形态注册(幂等:同名替换,可重复调用)。
503
+
504
+ 链序(2026-09-19 起):
505
+ before = linkref(解析) → deps(依赖声明) → audit → consistency → gated
506
+ → 链尾执行器
507
+ after = linkref(建边) → trust(一跳传播)
508
+
509
+ linkref 置于链首的理由:正文引用解析是**纯读、无副作用**,且其结果必须
510
+ 先于任何短路闸写入 ctx,供 after 链消费。短路闸(REJECT/DEFER/gated)
511
+ 返回终态时 `execute` 不跑 after 链,故未落盘的写入不会建边——语义正确。
512
+
513
+ deps 夹在 linkref 与 audit 之间的理由:两件事都与内容政策无关,故在 audit
514
+ 之前;linkref 之后是因为它只解析正文引用、不动依赖声明——依赖是**字段域**
515
+ 而非正文域,顺序倒置不会互相污染,但保持「解析在前、裁决在后」的一致读序。
516
+
517
+ after 的 trust 置于 linkref 之后:建边先于传播——传播按 `depends_on`
518
+ 反查(字段域)而非边域,故顺序不影响正确性;置于其后只为让 ctx 中的
519
+ 边信息先落定,便于排障时读 ctx。
520
+
521
+ linkref 的**落点是 after 而非注入 `a["edges"]`**:`cg.add` 是全量重建
522
+ fm,注入 edges 会在覆写既有节点时清空其原有边(破坏性副作用);
523
+ `append_edge` 是边域窄原语且幂等(见 linkref 模块 docstring)。
524
+ 依赖声明同忌经 `a["edges"]` 注入——走 `depends_on` 字段域。
525
+ """
526
+ from . import linkref
527
+ pipe.register_before("linkref", linkref.before_hook(), position=0)
528
+ pipe.register_before("deps", _gate_deps)
361
529
  pipe.register_before("audit", _gate_audit)
362
530
  pipe.register_before("consistency", _gate_consistency)
363
531
  pipe.register_before("gated", _gate_gated)
532
+ pipe.register_after("linkref", linkref.after_hook())
533
+ pipe.register_after("trust", _after_trust)
364
534
  return pipe
365
535
 
366
536
 
537
+ # 生效条件:模块级 _DEFAULT 为 None 时新建 WritePipeline 并经 install_default_gates 注册后缓存返回,否则直接返回已缓存的 _DEFAULT 单例;
367
538
  def default_pipeline():
368
539
  """进程级默认写入链(单例)。自定义闸门 register_before 即插即拔。"""
369
540
  global _DEFAULT
370
541
  if _DEFAULT is None:
371
542
  _DEFAULT = install_default_gates(WritePipeline())
372
- return _DEFAULT
543
+ return _DEFAULT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@furongjun1999/dsh-memory",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "description": "灵枢(Lingshu·líng shū)DeepSeek Harness 插件:完整大脑——长期记忆/知识飞轮/自我认知/递归反思接入 DSH,对话自动沉淀进 md_cg 认知图(md 文档)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -78,7 +78,7 @@
78
78
  "scripts": {
79
79
  "build": "tsc -p tsconfig.json",
80
80
  "test": "npm run build && node --import tsx --test \"test/*.test.ts\"",
81
- "prepare": "npm run build",
81
+ "prepare": "node -e \"const fs=require('fs');const tsc='node_modules/typescript/bin/tsc';fs.existsSync(tsc)?require('child_process').execFileSync(process.execPath,[tsc,'-p','tsconfig.json'],{stdio:'inherit'}):console.log('[prepare] 跳过构建:typescript 未安装(NODE_ENV=production 或 --omit=dev 会省略 devDependencies)——需要构建请用 npm install --include=dev')\"",
82
82
  "prepublishOnly": "npm run build && npm test"
83
83
  },
84
84
  "repository": {
@@ -50,6 +50,7 @@ DIM_KEYWORDS = (
50
50
  )
51
51
 
52
52
 
53
+ # 生效条件:对 sys.stdout 与 sys.stderr 依次尝试 reconfigure(encoding="utf-8"),任何异常都被 try/except 吞掉后继续;
53
54
  def _stdout_utf8():
54
55
  for stream in (sys.stdout, sys.stderr):
55
56
  try:
@@ -58,6 +59,7 @@ def _stdout_utf8():
58
59
  pass
59
60
 
60
61
 
62
+ # 生效条件:text 为 None 或空串时返回 default,text 已是 dict/list 时原样返回,否则返回 json.loads(text) 的结果;
61
63
  def _load_json_arg(text, default):
62
64
  if text is None or text == "":
63
65
  return default
@@ -66,12 +68,14 @@ def _load_json_arg(text, default):
66
68
  return json.loads(text)
67
69
 
68
70
 
71
+ # 生效条件:value 为假值(None/""/[]/{}/0)时返回 [],value 是 str 时返回 [value],其余返回 list(value);
69
72
  def _as_list(value):
70
73
  if not value:
71
74
  return []
72
75
  return [value] if isinstance(value, str) else list(value)
73
76
 
74
77
 
78
+ # 生效条件:text 为假值时先置为 "",按 DIM_KEYWORDS 顺序返回首个含命中关键词的维度,全部未命中返回 "条件边界";
75
79
  def classify_dimension(text):
76
80
  """把失配描述归入五失配维度之一;未命中归入「条件边界」。"""
77
81
  text = text or ""
@@ -81,10 +85,12 @@ def classify_dimension(text):
81
85
  return "条件边界"
82
86
 
83
87
 
88
+ # 生效条件:key 在 FOUR_DIMS 中时原样返回 key,否则返回 classify_dimension(key) 的归类结果;
84
89
  def _dim_of_key(key):
85
90
  return key if key in FOUR_DIMS else classify_dimension(key)
86
91
 
87
92
 
93
+ # 生效条件:conditions 为假值(None/""/{})时返回 {},conditions 是 str 时先 json.loads 再 dict(...),其余直接 dict(conditions);
88
94
  def normalize_conditions(conditions):
89
95
  if not conditions:
90
96
  return {}
@@ -95,6 +101,7 @@ def normalize_conditions(conditions):
95
101
 
96
102
  # ---------------------------------------------------------------- 步骤 1:声明
97
103
 
104
+ # 生效条件:position 为假值时回落 "designer",不在 POSITION_MAP 中时 raise SystemExit,space 假值时用空 OrderedDict 并 setdefault 观测位置,返回含 space_id、layer、missing_dims 的 OrderedDict;
98
105
  def declare_core(position="designer", space=None):
99
106
  """条件空间声明:四维 + 有效范围 + 切换规则(3.1/3.2)。"""
100
107
  position = position or "designer"
@@ -115,6 +122,7 @@ def declare_core(position="designer", space=None):
115
122
 
116
123
  # ---------------------------------------------------------------- 步骤 2-3:白箱三问 + 四态
117
124
 
125
+ # 生效条件:evidence 取 candidate["evidence"] or conditions["anchors"] 再 _as_list,vpath 取 verification_path or candidate["verification_path"] 再 _as_list;evidence 为空返回 (False,"观测工具",detail),否则 conditions 缺任一 FOUR_DIMS 时返回 (False, 首个缺失维度, detail),否则 vpath 为空返回 (False,"条件边界",detail),全齐返回 (True,None,detail);
118
126
  def whitebox_check(candidate, conditions, verification_path=None):
119
127
  """白箱三问(第〇章)。返回 (ok, missing_dim, detail)。"""
120
128
  evidence = _as_list(candidate.get("evidence") or conditions.get("anchors"))
@@ -134,6 +142,7 @@ def whitebox_check(candidate, conditions, verification_path=None):
134
142
  return True, None, detail
135
143
 
136
144
 
145
+ # 生效条件:candidates 为假值(None/[])判 BLINDSPOT,否则逐候选按 not_applicable 与 query 的冲突记 REJECT、when 与 conditions 不符记 DEFER,全为 REJECT 判 REJECT、无合格候选取首个 DEFER、多个合格候选仅唯一 distinguishing 时选之否则 DEFER,verdict 仍空时由 whitebox_check(cand, conditions, verification_path) 定 ACCEPT 或 DEFER;
137
146
  def judge_core(query, conditions=None, candidates=None, verification_path=None):
138
147
  """四态资格裁决(6章.2):先拒绝,后接受。"""
139
148
  conditions = normalize_conditions(conditions)
@@ -248,6 +257,7 @@ def emit_mcp_calls(result):
248
257
  return calls
249
258
 
250
259
 
260
+ # 生效条件:从 result["conditions"] 取四维(.get 缺键显示 <未声明>)、从 result["traces"] 中首个含 "whitebox" 的项渲染白箱行,emit_mcp 为真值时追加 emit_mcp_calls(result) 的 JSON,返回 '\n'.join(lines);
251
261
  def render_verdict(result, emit_mcp=False):
252
262
  c = result["conditions"]
253
263
  lines = ["[判定单]",
@@ -275,6 +285,7 @@ def render_verdict(result, emit_mcp=False):
275
285
 
276
286
  # ---------------------------------------------------------------- 步骤 5:归因 / 蒸馏
277
287
 
288
+ # 生效条件:error 为假值时先 dict({}),missing 取 error.get("missing",""),dim 取 error.get("dimension") or classify_dimension(missing) 且不在 FIVE_MISMATCH 时回落 "条件边界",返回含 triple/mismatch_dim/discipline 的 OrderedDict;
278
289
  def attribute_core(error):
279
290
  """条件层归因:三方条件空间失配 + 五失配维度定位(附录22.3)。"""
280
291
  error = dict(error or {})
@@ -297,6 +308,7 @@ def attribute_core(error):
297
308
  ])
298
309
 
299
310
 
311
+ # 生效条件:对 rows 每行按 dimension 或 classify_dimension(row.get("missing","")) 分组(不在 FIVE_MISMATCH 归入 "条件边界"),每组以首行 symptom/missing 生成一条带 when/rule/change 的纪律项,rows 为空返回空 items 列表;
300
312
  def distill_core(rows):
301
313
  """失败集合 → 带适用条件的可复用纪律(1.1.1)。"""
302
314
  groups = defaultdict(list)
@@ -318,6 +330,7 @@ def distill_core(rows):
318
330
  return items
319
331
 
320
332
 
333
+ # 生效条件:对 items 逐项(依赖 it["missing"]/["rule"]/["when"]/["change"]/["evidence"])编号渲染 markdown 段落,items 为空时返回仅含标题与说明的文本;
321
334
  def render_distill(items):
322
335
  lines = ["# 蒸馏纪律(失败资产化)", "",
323
336
  "> 失败 → 根因(条件层)→ 可复用纪律(带适用条件)→ 注入 → 不再犯(1.1.1)", ""]
@@ -334,6 +347,7 @@ def render_distill(items):
334
347
 
335
348
  # ---------------------------------------------------------------- probe / selftest
336
349
 
350
+ # 生效条件:按 os.environ.get("DMCG_HOME") 再 DEFAULT_DMCG_HOME 顺序,跳过假值或不含 md_cg 子目录的 base,命中的 base 插入 sys.path 后 import md_cg.mdcos,成功返回 available=True/mode=read-only,import 抛异常返回 available=False/degraded 附 error,全部未命中返回 available=False、base=None;
337
351
  def probe_mdcg():
338
352
  """探测 md_cg 认知图可用性(只读 import,不实例化、不写库)。"""
339
353
  bases = [os.environ.get("DMCG_HOME"), DEFAULT_DMCG_HOME]
@@ -358,6 +372,7 @@ def probe_mdcg():
358
372
  ("error", "未找到 md_cg(可设 DMCG_HOME 指向 dsh-memory 根目录)")])
359
373
 
360
374
 
375
+ # 生效条件:按 case.get("kind") 分派 —— declare 比对 declare_core 的 missing_dims 与 expect 的 dims_complete/observation_position_contains,judge 与 verify 比对 judge_core 的 verdict/missing_dim 与 expect 的 state/missing_dim/require_verification_path/require_missing_dim,missing 比对 attribute_core 的 mismatch_dim,其余 kind 记 ok=False;
361
376
  def run_cases(cases):
362
377
  results = []
363
378
  for case in cases:
@@ -397,6 +412,7 @@ def run_cases(cases):
397
412
 
398
413
  # ---------------------------------------------------------------- CLI
399
414
 
415
+ # 生效条件:items 为假值(None/空列表)时返回空 OrderedDict,否则每项不含 "=" 时 raise SystemExit,含 "=" 的按首个 "=" 拆分并 strip 后写入 space;
400
416
  def _parse_space(items):
401
417
  space = OrderedDict()
402
418
  for it in items or []:
@@ -527,4 +543,4 @@ def main(argv=None):
527
543
 
528
544
 
529
545
  if __name__ == "__main__":
530
- sys.exit(main())
546
+ sys.exit(main())
@@ -21,6 +21,7 @@ sys.path.insert(0, os.path.join(ROOT, "scripts"))
21
21
  import designer # noqa: E402 (路径注入后导入)
22
22
 
23
23
 
24
+ # 生效条件:读取 cases_path 的 JSONL(strip 后为空的行跳过)交给 designer.run_cases,返回 (results, passed, total) 三元组;
24
25
  def run(cases_path):
25
26
  with open(cases_path, encoding="utf-8") as fh:
26
27
  cases = [json.loads(line) for line in fh if line.strip()]
@@ -30,6 +31,7 @@ def run(cases_path):
30
31
  return results, passed, total
31
32
 
32
33
 
34
+ # 生效条件:argv(None 时取 sys.argv)解析后 --cases 缺省为 HERE/cases.jsonl,调用 run 后 passed==total 返回 0 否则返回 1,--json 为真值时打印 JSON 汇总、否则打印逐项明细;
33
35
  def main(argv=None):
34
36
  ap = argparse.ArgumentParser(prog="selftest.py",
35
37
  description="设计者视角 · 认知能力验收")
@@ -57,4 +59,4 @@ def main(argv=None):
57
59
 
58
60
 
59
61
  if __name__ == "__main__":
60
- sys.exit(main())
62
+ sys.exit(main())
package/src/hooks.ts CHANGED
@@ -21,6 +21,9 @@
21
21
  * 模型请求组装 system prompt 时自动注入灵枢最近记忆
22
22
  * (`stg(op=timeline)`,最近记忆节点时间线),让记忆"自动可用"而不只依赖
23
23
  * Agent 主动调用 recall/think 工具。失败静默(不影响请求)。
24
+ * ⚠️ 该注入块的**稳定性**决定宿主是否新追加快照:内容没变时也必须照旧 push
25
+ * (宿主按渲染后的整段文本去重);跳过 push 反而会各追加一份「有块/无块」的快照
26
+ * —— 详见 installMemoryHooks 里的长注释。
24
27
  *
25
28
  * ⚠️ 注入文本**必经** escapePromptBraces(src/lib/prompt_safety.ts,issue #16):
26
29
  * 宿主对 context 文本做严格 `{{variable}}` 插值,裸 `{{` 会让每轮 assemble 抛错
@@ -126,8 +129,6 @@ const RECALL_MIN_CHARS = 24
126
129
  const RECALL_MAX_CHARS = 1400
127
130
  /** 永久层不自动注入(按需用 mdcg_recall / lingshu_stg 取)。 */
128
131
  const RECALL_SKIP_LAYERS = new Set(['anchor', 'self'])
129
- /** 连续跳过多少步后强制补一次(内容未变也刷新,防止压缩归档后块消失)。 */
130
- const RECALL_REPUSH_EVERY = 8
131
132
 
132
133
  /** 分级递减渲染:按距当前的次序逐档收窄,早期条目信息量更大。
133
134
  *
@@ -159,17 +160,6 @@ function formatTimelineDecayed(payload: unknown): string {
159
160
  return out.join('\n').slice(0, RECALL_MAX_CHARS)
160
161
  }
161
162
 
162
- /** 去重:内容与上次相同就不再 push 新副本。
163
- *
164
- * 背景:本钩子挂在 `system-prompt/assemble` 上,每个 step 都会 push 一份,而每份
165
- * 都会留成独立的 surface 节点 → 同一块累积 N 份(实测 11 份 ≈2700 tok/请求),
166
- * 随步数线性增长。去重后同一块只占 1 份,外加每 RECALL_REPUSH_EVERY 步一次自愈刷新。 */
167
- function shouldPushRecall(text: string, lastText: string, skippedSincePush: number): boolean {
168
- if (!text) return false
169
- if (text !== lastText) return true
170
- return skippedSincePush >= RECALL_REPUSH_EVERY
171
- }
172
-
173
163
  /** 安装自动记忆钩子(effect 作用域内,随插件卸载自动移除)。
174
164
  *
175
165
  * mdcg 为 null(config.mdcg.enabled=false)时自动记忆整体停用:记忆真源是
@@ -200,19 +190,27 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
200
190
 
201
191
  // P1 完善(自动 recall 注入):每次模型请求组装 system prompt 时,注入灵枢最近记忆。
202
192
  // 用 system-prompt/assemble 事件(waterfall)而非 llm/stream——后者请求 deep-frozen 不可改写。
193
+ //
194
+ // ⚠️ 必须**每步都 push**,哪怕内容与上一步逐字节相同。原因在宿主侧(dsh-agent-loop 的
195
+ // RuntimeContextProjection):assembly.contexts 会被渲染成一段「运行时上下文快照」,
196
+ // 每个 step 拿渲染后的**整段文本**与上一份已提交的快照比对,**只有不同才**在会话里
197
+ // append 一条新的 user/message(append 语义,旧的不会被替换或移除)。于是:
198
+ // · 内容不变 + 照旧 push → 渲染文本不变 → 宿主不追加任何东西(零开销、零增长);
199
+ // · 内容不变 + 跳过 push → 渲染文本**变了**(少了本块)→ 宿主追加一份「没有本块」的
200
+ // 快照;下一步再 push 又把本块加回来 → **再**追加一份。跳过一次反而多花两份快照
201
+ // (实测每份 ~250 tok),这正是 v0.4.8「每 8 步强制补一次」的自愈刷新会把长会话的
202
+ // inject 推到 30k+ tok 的原因。
203
+ // 因此本实现把「要不要补」交还给宿主:压缩归档后宿主会把 retained 置空并重新投影快照
204
+ // (RuntimeContextProjection 的 retained === null 分支),本块自然跟着回来——
205
+ // 不需要插件自己数步数做自愈。
203
206
  if (opts.autoRecall) {
204
207
  const recallLimit = Math.max(1, Math.min(10, opts.autoRecallLimit || 4))
205
- // 去重状态:同一块内容只保留一份 surface 节点,避免随步数线性增长。
206
- let lastRecallText = ''
207
- let skippedSincePush = 0
208
208
  ctx.on('system-prompt/assemble', async (assembly, _ctx, next) => {
209
209
  try {
210
210
  // 异步取最近记忆节点(失败静默——不阻塞模型请求)
211
211
  if (graph.isReady()) {
212
212
  const text = formatTimelineDecayed(await graph.timeline(recallLimit))
213
- if (shouldPushRecall(text, lastRecallText, skippedSincePush)) {
214
- lastRecallText = text
215
- skippedSincePush = 0
213
+ if (text) {
216
214
  // 注入边界转义(issue #16):宿主 system-prompt 对 context 文本做严格
217
215
  // `{{variable}}` 插值,裸 `{{` 会 throw → 该轮请求整体失败。记忆原文
218
216
  // (含用户命令里的 `{{.X}}`)必须保真落库,故只在注入副本上打断 `{{`。
@@ -220,8 +218,6 @@ export function installMemoryHooks(ctx: Context, mdcg: MdcgClient | null, opts:
220
218
  name: 'lingshu:auto-recall',
221
219
  text: escapePromptBraces(`【灵枢最近记忆】\n${text.slice(0, RECALL_MAX_CHARS)}`),
222
220
  })
223
- } else {
224
- skippedSincePush += 1
225
221
  }
226
222
  }
227
223
  }
package/src/index.ts CHANGED
@@ -17,8 +17,10 @@
17
17
  * name: '@furongjun1999/dsh-memory'
18
18
  * config:
19
19
  * mdcg: # 记忆真源:认知图(md 文档)
20
- * # root = 记忆写入路径(用户可改)。缺省 = 插件仓自身 data/mdcg。
21
- * # 覆盖优先级:env MDCG_ROOT > <插件仓>/data/paths.json > 本项 > 默认
20
+ * # root = 记忆写入路径(用户可改)。缺省 = 用户级状态根 data/mdcg
21
+ * # (~/.dsh/.dsh-memory/data/mdcg;**不在插件包内**——包内数据会被
22
+ * # pnpm 更新连目录一起删掉,见 src/lib/datapath.ts 头注)
23
+ * # 覆盖优先级:env MDCG_ROOT > <用户级状态根>/paths.json > 本项 > 默认
22
24
  * # root: 'D:/somewhere/mdcg' # 例:把记忆库放到别处
23
25
  * actor: 'dsh-memory'
24
26
  * env: # 写入凭据:默认【关闭】,由你决定是否打开
@@ -52,7 +54,7 @@ import { installMemoryHooks, type MemoryHooksOptions } from './hooks.js'
52
54
  // LIB 本地库:角色扮演网页 / 互维维护 / 白箱 LLM 适配器统一收在 src/lib/。
53
55
  import { installRoleplayWeb } from './lib/roleplay_web.js'
54
56
  import { MdcgClient } from './lib/mdcg_client.js'
55
- import { describeDataPaths, mdcgRoot } from './lib/datapath.js'
57
+ import { describeDataPaths, migrateLegacyData, mdcgRoot, repoRoot } from './lib/datapath.js'
56
58
  // 写入凭据密钥环(首启引导):显式配置 → ~/.mdcg/token → 首启自动签发。
57
59
  import { resolveToken, type TokenResolution } from './lib/token_store.js'
58
60
 
@@ -178,8 +180,9 @@ export const Config: z<Config> = z.object({
178
180
  .default({ enabled: false, heartbeatMs: 10 * 60 * 1000 }),
179
181
  /** 认知图(md_cg):记忆唯一真源。
180
182
  * root 为**记忆写入路径**(用户可改)。缺省空串 = 未指定,按优先级解析:
181
- * ① env MDCG_ROOT ② `<插件仓>/data/paths.json` 的 root
182
- * ③ 本项 ④ 默认 `<插件仓>/data/mdcg`
183
+ * ① env MDCG_ROOT ② `<用户级状态根>/paths.json` 的 root
184
+ * (旧 `<插件仓>/data/paths.json` 兼容读)
185
+ * ③ 本项 ④ 默认 `<用户级状态根>/data/mdcg`
183
186
  * 相对路径一律相对**插件仓根**解析——历史教训:相对 cwd 的相对路径随
184
187
  * 宿主 cwd 漂移,cwd 落在别仓时记忆真源分裂成互不可见的两处。
185
188
  * tenant/actor 决定私有内容加解密的身份:与 migrate_roleplay 的
@@ -255,15 +258,39 @@ export async function apply(ctx: Context, config: Config): Promise<void> {
255
258
  timeoutMs: config.toolCallTimeoutMs,
256
259
  maxRetryDelayMs: config.maxRetryDelayMs,
257
260
  })
261
+ // 数据面迁出插件包(issue #18 相邻问题):旧版把运行时数据与路径配置写在包内
262
+ // `<pkg>/data`,pnpm 更新会连目录一起替换(实机实证:data/ 54 文件 → 0,
263
+ // 46 条记忆节点靠人工备份回填)。首启把「旧位置仍有货」的数据**复制**到用户级
264
+ // 数据根;必须在 mdcg.start() 之前——store 要在 Python 子进程接管前就位。
265
+ const migrated = migrateLegacyData()
266
+ if (migrated.ran) {
267
+ ctx.logger.info(
268
+ `dsh-memory: 数据面已迁出插件包(旧位置只复制未删除):`
269
+ + `${migrated.from} → ${migrated.to}(${migrated.copied} 项`
270
+ + `${migrated.failed.length ? `,失败 ${migrated.failed.join(',')}` : ''})`,
271
+ )
272
+ }
258
273
  mdcg.start()
259
274
  // 启动留痕:把「记忆真源在哪、由谁决定、路径是否存在」写进可审计日志,
260
275
  // 避免再次出现「以为在记忆、其实写到了别仓」的静默分裂(历史事故)。
261
276
  const dp = describeDataPaths(config.mdcg.root)
262
277
  ctx.logger.info(
263
278
  `dsh-memory: 记忆真源路径 = ${dp.mdcgRoot}(来源 ${dp.source},`
264
- + `dataRoot=${dp.dataRoot},存在=${dp.mdcgRootExists ? '是' : '否(首次写入将创建)'},`
279
+ + `状态根=${dp.stateRoot},dataRoot=${dp.dataRoot},`
280
+ + `存在=${dp.mdcgRootExists ? '是' : '否(首次写入将创建)'},`
265
281
  + `用户可改:${dp.pathsFile})`,
266
282
  )
283
+ // 包管理器装的插件 + 路径配置还在旧包内位置:pnpm 下次更新会把该文件连目录一起
284
+ // 删除,用户配置随之丢失(回落默认根 → 表现为「记忆不见了」)。只在真的会被删的
285
+ // 布局下提醒(开发用的 git clone 不含 node_modules 段 → 不打扰)。
286
+ const pnpmManaged = repoRoot().split(/[\\/]/).includes('node_modules')
287
+ if (pnpmManaged && String(dp.pathsFileSource) === 'legacy') {
288
+ ctx.logger.warn(
289
+ `dsh-memory: 路径配置仍在插件包内(${dp.pathsFile})——pnpm 更新该包会连目录`
290
+ + `一起删除。请复制到 ${String(dp.stateRoot)}/paths.json 后重启`
291
+ + `(数据面已自动复制到 ${String(dp.dataRoot)})。`,
292
+ )
293
+ }
267
294
  brainReady = await mdcg.waitReady()
268
295
  if (brainReady) {
269
296
  ctx.logger.info(`dsh-memory: 认知图已就绪(MDCG_ROOT=${resolvedRoot})`)