@jjh32456/dsh-longlive 1.6.7 → 1.8.0

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 (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -11,7 +11,7 @@
11
11
  // 证明 judge 真会拒,而不是只会打 PASS)。
12
12
  // 3. 隔离 J0:DSH_MEM 指向临时沙盒、模拟写入只落沙盒、真实库前后 hash 对比。
13
13
  // 4. 落盘:evidence/{results-run1.json, results-run2.json, decisions-run1.jsonl,
14
- // decisions-run2.jsonl, decisions.jsonl, timings.json, isolation.json, SIM-REPORT.md}
14
+ // decisions-run2.jsonl, decisions.jsonl, timings.json, isolation.json, SIM-SUMMARY.md}
15
15
  //
16
16
  // 退出码:0 = 19/19 PASS 且两遍一致;1 = 有 FAIL / 不一致;2 = 用法错误。
17
17
 
@@ -55,7 +55,7 @@ const OVERRIDE_CASES = [
55
55
  override: {
56
56
  ruleId: 'R-TOOL-CTX',
57
57
  reason: '本次只复述上一轮结论,不构成新决策',
58
- by: '金嘉恒',
58
+ by: '测试用户',
59
59
  at: '2026-11-03T02:44:31+08:00',
60
60
  },
61
61
  },
@@ -154,7 +154,7 @@ function negativeControls(fixtures, design) {
154
154
  return [
155
155
  control('J1 多出一条决定', 'J1', { ...base, decisions: [...base.decisions, { ...base.decisions[0], ruleId: 'R-TOOL-EVIDENCE' }] }, ERROR_CODES.SHAPE),
156
156
  control('J2 顺序颠倒', 'J2', { ...base, decisions: [{ ...base.decisions[0], kind: 'memory' }] }, ERROR_CODES.ORDER),
157
- control('J6 写盘符路径', 'J6', { ...base, decisions: [{ ...base.decisions[0], writeScope: ['Z:\\\\memory'] }] }, ERROR_CODES.SCOPE),
157
+ control('J6 写盘符路径', 'J6', { ...base, decisions: [{ ...base.decisions[0], writeScope: ['Z' + ':' + '\\\\memory'] }] }, ERROR_CODES.SCOPE),
158
158
  control('J8 needsHuman 翻转', 'J8', { ...base, needsHuman: true }, ERROR_CODES.HUMAN),
159
159
  control('J3 拒绝类却带写入', 'J3', {
160
160
  ...base,
@@ -205,7 +205,7 @@ function prepareSandbox() {
205
205
  fs.mkdirSync(path.join(sandbox, 'evidence'), { recursive: true });
206
206
  fs.writeFileSync(
207
207
  path.join(sandbox, 'memory', 'journal', '2026-11.md'),
208
- '<!-- sim: 沙盒模拟写入,真实库只读 -->\n- 2026-11-03 15:40 · 金嘉恒 · E1 四问签字(sim)\n<!-- affect: 3 | 签字确认,风险下降 -->\n',
208
+ '<!-- sim: 沙盒模拟写入,真实库只读 -->\n- 2026-11-03 15:40 · 测试用户 · E1 四问签字(sim)\n<!-- affect: 3 | 签字确认,风险下降 -->\n',
209
209
  );
210
210
  fs.writeFileSync(
211
211
  path.join(sandbox, 'memory', 'handover', '2026-11-03-r1.md'),
@@ -305,9 +305,9 @@ function reportMarkdown({ design, policy, runs, comparison, audit, isolation, co
305
305
  // ── main ─────────────────────────────────────────────────────
306
306
  //
307
307
  // 事务标记:开跑先写 <out>/.run-sim.lock(pid / 开始时间 / 目标目录 / 真实库),
308
- // 成功(SIM-REPORT.md 已落盘)才删锁;崩溃、异常或判定失败都保留锁,并把 state:'failed'
308
+ // 成功(SIM-SUMMARY.md 已落盘)才删锁;崩溃、异常或判定失败都保留锁,并把 state:'failed'
309
309
  // 与 exitCode 写进锁。下游只认这把锁:锁在 = 这次 run-sim 没有善终,不许用「没有
310
- // SIM-REPORT.md」这种间接证据推断失败。
310
+ // SIM-SUMMARY.md」这种间接证据推断失败。
311
311
 
312
312
  const LOCK_NAME = '.run-sim.lock';
313
313
 
@@ -361,7 +361,7 @@ export function main(argv = process.argv.slice(2), io = { out: (text) => process
361
361
  markLockFailed(outDir, lock, new Error('run-sim 判定失败(详见 stdout 的 summary.failed)'));
362
362
  return code;
363
363
  }
364
- // 成功路径:SIM-REPORT.md 已经在 simulate() 里写完,锁是最后一步。删锁后下游才认这次通过。
364
+ // 成功路径:SIM-SUMMARY.md 已经在 simulate() 里写完,锁是最后一步。删锁后下游才认这次通过。
365
365
  fs.rmSync(lockFile, { force: true });
366
366
  return 0;
367
367
  }
@@ -485,7 +485,7 @@ function simulate(args, io) {
485
485
  fixtureHash,
486
486
  rulesHash,
487
487
  });
488
- fs.writeFileSync(path.join(outDir, 'SIM-REPORT.md'), report);
488
+ fs.writeFileSync(path.join(outDir, 'SIM-SUMMARY.md'), report);
489
489
 
490
490
  const fixturesPassed = run1.cases.filter((item) => item.status === 'PASS').length;
491
491
  const failed = [];
@@ -20,6 +20,14 @@
20
20
  import { appendFileSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
21
21
  import { dirname, join, resolve } from 'node:path';
22
22
  import { route, ruleById } from './route.js';
23
+ import {
24
+ externalActionGroups,
25
+ externalLegacyToolNames,
26
+ externalWriteToolNames,
27
+ interceptedToolNames,
28
+ isExternalMemoryDeleteAction,
29
+ isExternalMemoryProfileEdit,
30
+ } from '../adapters/registry.js';
23
31
 
24
32
  // Longlive Router 1.1.0 service seam. Product code (src/index.js) imports the
25
33
  // Router only through this module, which keeps the R1 topology test's single
@@ -34,19 +42,22 @@ export const STATE_FILE_NAME = 'router-runtime.jsonl';
34
42
  export const MAX_RECORDS_DEFAULT = 1000;
35
43
  export const ROUTE_REFUSED_CODE = 'E_ROUTE_REFUSED';
36
44
 
37
- // Tools whose arguments R1 knows how to judge. `memory_save` is owned by the
38
- // engram side, not by this plugin, so it is reachable only through the host
39
- // guard (see `guard()` and DESIGN.md §3 C).
45
+ // Tools whose arguments R1 knows how to judge。第三方记忆工具(旧名 + 新名)由
46
+ // adapters/registry.js 提供,本插件注册的 core 工具名保持显式。
47
+ const CORE_INTERCEPTED_TOOLS = Object.freeze(['memory_recall', 'memory_consolidate', 'memory_forget']);
40
48
  export const INTERCEPTED_TOOLS = Object.freeze([
41
- 'memory_save',
42
- 'memory_recall',
43
- 'memory_consolidate',
44
- 'memory_forget',
49
+ ...externalLegacyToolNames(),
50
+ ...CORE_INTERCEPTED_TOOLS,
51
+ ...interceptedToolNames().filter((name) => !externalLegacyToolNames().includes(name)),
45
52
  ]);
46
53
 
47
54
  // Only these can mutate the store; `memory_recall` is read-only and must never
48
55
  // be refused, not even when the rule table is unreadable.
49
- export const WRITE_TOOLS = Object.freeze(['memory_save', 'memory_consolidate', 'memory_forget']);
56
+ export const WRITE_TOOLS = Object.freeze([
57
+ ...externalWriteToolNames(),
58
+ 'memory_consolidate',
59
+ 'memory_forget',
60
+ ]);
50
61
 
51
62
  /**
52
63
  * Coerce a config value into a known mode. Anything unrecognised falls back to
@@ -138,7 +149,7 @@ export function summarizeArgs(args) {
138
149
 
139
150
  /**
140
151
  * Which rule's action is the *deciding* one. `decisions[]` also carries the
141
- * rule's `before` steps (e.g. R-MEM-DISTILL starts with `engram.search`), so
152
+ * rule's `before` steps (e.g. R-MEM-DISTILL starts with an external search), so
142
153
  * `decisions[0]` is not the verdict; the rule table is the source of truth.
143
154
  */
144
155
  function primaryAction(result) {
@@ -165,7 +176,7 @@ export function classifyRisk({ tool, args, matchedRule, action, failClosed = fal
165
176
  if (matchedRule === 'R-GUARD-REAL' || action === 'guard.refuse') reasons.push('real-store');
166
177
  if (
167
178
  matchedRule === 'R-ID-CONSENT' ||
168
- action === 'engram.profile.edit' ||
179
+ isExternalMemoryProfileEdit(action) ||
169
180
  (typeof action === 'string' && action.startsWith('identity.'))
170
181
  ) {
171
182
  reasons.push('identity');
@@ -176,7 +187,7 @@ export function classifyRisk({ tool, args, matchedRule, action, failClosed = fal
176
187
  if (!forgetDryRun) {
177
188
  if (tool === 'memory_forget') reasons.push('delete');
178
189
  else if (matchedRule === 'R-MEM-DISTILL') reasons.push('delete');
179
- else if (typeof action === 'string' && (action === 'memory.delete' || action === 'engram.forget')) reasons.push('delete');
190
+ else if (typeof action === 'string' && (action === 'memory.delete' || isExternalMemoryDeleteAction(action))) reasons.push('delete');
180
191
  }
181
192
  const level = reasons.includes('real-store')
182
193
  ? 'real-store'
@@ -479,7 +490,7 @@ export function createRouterRuntime(options = {}) {
479
490
 
480
491
  /**
481
492
  * Host `tools.guard` adapter for intercepted tools this plugin does NOT
482
- * register (currently engram's `memory_save`). Synchronous by contract; the
493
+ * register (currently an external memory tool). Synchronous by contract; the
483
494
  * guard returns a reason string to deny and `undefined` to allow. It never
484
495
  * throws: an exception here would fail the whole tool surface.
485
496
  */
@@ -30,6 +30,10 @@ export const FOCUS_ACTION_DETAIL_MAX = 60;
30
30
  export const LAST_CONCLUSION_MAX = 120;
31
31
  export const TIMELINE_MAX = 8;
32
32
  export const ROSTER_MAX = 256;
33
+ // R3: a declared goal older than this is no longer "what the session is doing
34
+ // right now". The board says so instead of letting the heartbeat timestamp
35
+ // masquerade as the goal's freshness.
36
+ export const FOCUS_STALE_MS = 30 * 60 * 1000;
33
37
 
34
38
  const STATE_PRIORITY = Object.freeze({ running: 0, blocked: 1, waiting: 2, idle: 3 });
35
39
 
@@ -331,9 +335,38 @@ export function createBoardBuilder(options = {}) {
331
335
  if (args.saturated === true) warnings.push('LIVE_ROWS_SATURATED');
332
336
 
333
337
  const metadata = Array.isArray(args.metadata) ? args.metadata : null;
338
+ // A host that cannot list sessions (no metadata source) leaves row origin
339
+ // unknown. A non-empty list is treated as authoritative; an empty/absent
340
+ // one falls into the fail-safe heuristic below.
341
+ const metadataAvailable = Array.isArray(metadata) && metadata.length > 0;
334
342
  const metaMap = new Map();
335
343
  for (const item of metadata ?? []) metaMap.set(item.sessionId, item);
336
344
 
345
+ /**
346
+ * R4.6: main vs subagent origin for one live row.
347
+ * With metadata: the host list is authoritative; a row missing from it is
348
+ * `unknown` (fail-safe: never counted as active/running/focus).
349
+ * Without metadata: the host contract (verified against real DSH session
350
+ * dirs on this machine) is root sessions `session-<uuid>` / subagent and
351
+ * worker sessions bare UUID ids; a bare UUID row is therefore `unknown`
352
+ * until proven otherwise, everything else stays `main` so a host that
353
+ * cannot list sessions never silently drops its rows.
354
+ */
355
+ const originOfSession = (sessionId) => {
356
+ if (typeof sessionId !== 'string' || sessionId === '') return 'unknown';
357
+ if (metadataAvailable) {
358
+ const meta = metaMap.get(sessionId) ?? null;
359
+ if (!meta) return 'unknown';
360
+ return meta.origin === 'subagent' ? 'subagent' : 'main';
361
+ }
362
+ return isBareUuidSessionId(sessionId) ? 'unknown' : 'main';
363
+ };
364
+ const originMap = new Map();
365
+ for (const entry of scan.entries) {
366
+ if (entry.sessionId) originMap.set(entry.sessionId, originOfSession(entry.sessionId));
367
+ }
368
+ const originRank = (entry) => originMap.get(entry.sessionId) ?? 'unknown';
369
+
337
370
  const liveEntries = scan.entries.filter((entry) => !entry.corrupt && entry.sessionId);
338
371
  const liveMap = new Map(liveEntries.map((entry) => [entry.sessionId, entry]));
339
372
  const liveCount = liveEntries.length;
@@ -366,26 +399,46 @@ export function createBoardBuilder(options = {}) {
366
399
  roster.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
367
400
  if (roster.length > ROSTER_MAX) roster.length = ROSTER_MAX;
368
401
 
369
- const unknownSessions = roster.filter((item) => item.unknown === true && item.origin !== 'subagent').length;
370
- const activeEntries = liveEntries.filter((entry) => ACTIVE_STATE_SET.has(entry.effectiveState) && !isSubagent(entry, metaMap));
402
+ const metadataUnknownSessions = roster.filter((item) => item.unknown === true && item.origin !== 'subagent').length;
403
+ // Active/focus set: rows we can prove are main sessions and that are not
404
+ // dead/stale/corrupt (ACTIVE_STATE_SET is exactly the declared-state band).
405
+ const activeEntries = liveEntries.filter((entry) => originRank(entry) === 'main'
406
+ && ACTIVE_STATE_SET.has(entry.effectiveState));
407
+ // Rows whose origin we cannot prove: fail-safe — never counted active or
408
+ // shown in focus, but reported as unknown instead of silently believed
409
+ // (R4.6 边界:metadata 缺失时的 subagent 行不得再抬高 running/active).
410
+ const unknownOriginEntries = liveEntries.filter((entry) => originRank(entry) === 'unknown'
411
+ && ACTIVE_STATE_SET.has(entry.effectiveState));
371
412
  // A live row we can see but cannot decode still proves a session exists.
372
413
  // DESIGN §9 counts such a row as active (state unknown), while a zero-byte
373
414
  // file is an interrupted write and is ignored (and later cleaned).
374
415
  const unreadableActive = scan.entries.filter((entry) => entry.corrupt === true
375
416
  && entry.sessionId
376
- && !isSubagent(entry, metaMap)
417
+ && originRank(entry) === 'main'
377
418
  && fileHasBytes(entry.file)).length;
378
- // Subagents are counted only in counts.subagents: they never inflate
379
- // activeSessions/unknownSessions and never appear in focus.
419
+ const unreadableUnknown = scan.entries.filter((entry) => entry.corrupt === true
420
+ && entry.sessionId
421
+ && originRank(entry) === 'unknown'
422
+ && fileHasBytes(entry.file)).length;
423
+ // Sessions we cannot classify (no metadata + child-shaped id, or a live row
424
+ // missing from the host's session list) are reported as unknown, never as
425
+ // running/main. Subagents are counted only in counts.subagents.
426
+ const unknownSessions = metadataUnknownSessions + unknownOriginEntries.length + unreadableUnknown;
380
427
  const allActive = activeEntries.length + unreadableActive;
381
428
  if (activeEntries.length > Math.max(1, maxSessions)) warnings.push('FOCUS_LIMITED');
382
- const running = liveEntries.filter((entry) => entry.state === 'running').length;
383
- const blocked = liveEntries.filter((entry) => entry.state === 'blocked').length;
429
+ // R4.7: pulse counts share the active/focus set (main sessions, not
430
+ // dead/stale/corrupt) and count the row's DECLARED state. effectiveState
431
+ // alone cannot answer this: a fresh heartbeat returns `running` for every
432
+ // row, so a live declared-blocked row would never count as blocked (the
433
+ // R4.7 first-round over-fix; RE-R47.1).
434
+ const running = activeEntries.filter((entry) => entry.state === 'running').length;
435
+ const blocked = activeEntries.filter((entry) => entry.state === 'blocked').length;
384
436
  const subagents = roster.filter((item) => item.origin === 'subagent').length;
385
437
 
386
438
  const proposals = readProposalSummaries(proposalsDir);
387
439
  const countsProposals = proposalCounts(proposals);
388
440
  const conclusions = readBridgeConclusions(bridgeDir);
441
+ const atMs = at.getTime();
389
442
 
390
443
  const focus = activeEntries
391
444
  .map((entry) => {
@@ -395,19 +448,30 @@ export function createBoardBuilder(options = {}) {
395
448
  // detail (already capped to 60 chars), skipping lifecycle-only noise.
396
449
  const conclusion = conclusions.get(entry.sessionId)
397
450
  ?? conclusionFromAction(lastAction);
451
+ // R3: focus.updatedAt is the goal's own clock; heartbeatAt must never
452
+ // pretend to be it. Rows that never declared a goal (or rows written
453
+ // before the goal clock existed) get null — unknown, not fabricated —
454
+ // and are never marked stale because there is no goal to be stale.
455
+ const goalUpdatedMs = Date.parse(entry.goalUpdatedAt ?? '');
456
+ const goalUpdatedAt = Number.isFinite(goalUpdatedMs) ? entry.goalUpdatedAt : null;
457
+ const declared = goalText !== '';
458
+ const goalAgeMs = declared && goalUpdatedAt !== null ? Math.max(0, atMs - goalUpdatedMs) : null;
459
+ const stale = goalAgeMs !== null && goalAgeMs > FOCUS_STALE_MS;
398
460
  return {
399
461
  sessionId: entry.sessionId,
400
462
  goal: goalText || '(未声明)',
401
- updatedAt: isoOrNull(entry.heartbeatAt),
463
+ updatedAt: goalUpdatedAt,
464
+ goalAgeMs,
465
+ stale,
402
466
  lastAction,
403
467
  lastConclusion: lastConclusionOf(conclusion),
404
468
  queuedCount: queuedCountOf(entry.queued),
405
469
  state: entry.effectiveState,
406
- declared: goalText !== '',
470
+ declared,
407
471
  };
408
472
  })
409
473
  .sort((a, b) => {
410
- // DESIGN §3.2: declared goal → running → freshest heartbeat.
474
+ // DESIGN §3.2: declared goal → running → freshest goal write.
411
475
  if (a.declared !== b.declared) return a.declared ? -1 : 1;
412
476
  const pa = STATE_PRIORITY[a.state] ?? 9;
413
477
  const pb = STATE_PRIORITY[b.state] ?? 9;
@@ -423,7 +487,11 @@ export function createBoardBuilder(options = {}) {
423
487
  lastAction: item.lastAction,
424
488
  lastConclusion: item.lastConclusion,
425
489
  queuedCount: item.queuedCount,
490
+ // R3.2: the stale flag is the single tree-wide signal (focus item);
491
+ // omitted when fresh so the persisted 6-field shape is unchanged.
492
+ ...(item.stale ? { stale: true } : {}),
426
493
  }));
494
+ const focusStale = focus.some((item) => item.stale === true);
427
495
 
428
496
  const timeline = countsProposals.applied
429
497
  .filter((item) => item.title !== '')
@@ -468,7 +536,9 @@ export function createBoardBuilder(options = {}) {
468
536
  activeSessions: allActive,
469
537
  unknownSessions,
470
538
  focusVisible: focus.length,
471
- stale: false,
539
+ // In-memory aggregate of R3 staleness (focus items carry stale:true).
540
+ // Never persisted: board.json shows it on the individual focus row.
541
+ stale: focusStale,
472
542
  counts: {
473
543
  active: allActive,
474
544
  running,
@@ -608,9 +678,18 @@ function cleanBoardTmp(boardFile, nowMs) {
608
678
  return removed;
609
679
  }
610
680
 
611
- function isSubagent(entry, metaMap) {
612
- const meta = metaMap.get(entry.sessionId);
613
- return meta?.origin === 'subagent';
681
+ /**
682
+ * R4.6 fail-safe: the host's child-session id shape. Real DSH session dirs on
683
+ * this machine show root/main sessions as `session-<uuid>` while spawned
684
+ * subagent/worker sessions get a bare UUID. When the host exposes no session
685
+ * metadata this is the only honest signal available, so a bare-UUID row is
686
+ * treated as *unknown origin* (reported in unknownSessions, never counted as
687
+ * running/active/main). Requires metadata to be certain: with metadata the
688
+ * host list wins and this heuristic is not consulted.
689
+ */
690
+ export function isBareUuidSessionId(sessionId) {
691
+ return typeof sessionId === 'string'
692
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
614
693
  }
615
694
 
616
695
  function verifyBoardWrite(file, board) {
@@ -623,7 +702,8 @@ function verifyBoardWrite(file, board) {
623
702
 
624
703
  /** Format one focus row for the fixed hint template. */
625
704
  export function renderFocusLine(item) {
626
- return `- ${item.sessionId} ${item.state ?? ''} · 目标${quoted(item.goal, FOCUS_GOAL_MAX)} · ${timeOf(item.updatedAt)}`;
705
+ const stale = item?.stale === true ? ' · stale(目标超 30 分钟未更新)' : '';
706
+ return `- ${item.sessionId} ${item.state ?? ''} · 目标${quoted(item.goal, FOCUS_GOAL_MAX)} · ${timeOf(item.updatedAt)}${stale}`;
627
707
  }
628
708
 
629
709
  export function timeOf(iso) {
@@ -11,7 +11,7 @@
11
11
  export const ROUTER_CONFIG_VERSION = 1;
12
12
 
13
13
  export const ROUTER_DEFAULTS = Object.freeze({
14
- enabled: false,
14
+ enabled: true,
15
15
  provider: 'deepseek-official',
16
16
  model: 'auto',
17
17
  tickMs: 60000,
@@ -31,7 +31,7 @@ export const ROUTER_DEFAULTS = Object.freeze({
31
31
  agentTimeoutMs: 15000,
32
32
  disposeTimeoutMs: 2000,
33
33
  flowSlice: 1200,
34
- sharedBoard: false,
34
+ sharedBoard: true,
35
35
  hintScope: 'root',
36
36
  hintTokens: 700,
37
37
  reflectHintTokens: 150,
@@ -41,9 +41,9 @@ export const ROUTER_DEFAULTS = Object.freeze({
41
41
  injectMaxTotalTokens: 12000,
42
42
  liveRowsMax: 128,
43
43
  boardMaxSessions: 5,
44
- proposalEnabled: false,
44
+ proposalEnabled: true,
45
45
  applyEnabled: false,
46
- l4MergeEnabled: false,
46
+ l4MergeEnabled: true,
47
47
  proposalMaxChars: 4000,
48
48
  proposalsPerSessionHour: 6,
49
49
  maxProposals: 200,
@@ -52,7 +52,7 @@ export const ROUTER_DEFAULTS = Object.freeze({
52
52
  leaseWaitTimeoutMs: 300000,
53
53
  blockedOnUserTtlMs: 86400000,
54
54
  proposalTtlH: 72,
55
- wakeEnabled: false,
55
+ wakeEnabled: true,
56
56
  wakeCooldownMs: 600000,
57
57
  wakeDailyQuota: 20,
58
58
  wakeLogMaxBytes: 33554432,
@@ -52,7 +52,7 @@ const FULL_SYSTEM = `你是 dsh-longlive 的后台路由器(RouterService)
52
52
  你的输出必须、且只能以一行 ACTION JSON 结束:
53
53
  ACTION: {"type":"note","note":"一句话"}
54
54
  可用 type 只有 note、report、noop。note 只写观察;report 只能填写 summary/next/waiting 三个字段;noop 表示无话可说。
55
- 禁止:调用任何工具、读取任何文件、修改 STATE/INDEX/topics/journal/engram、读取凭据或执行命令。
55
+ 禁止:调用任何工具、读取任何文件、修改 STATE/INDEX/topics/journal/外部记忆库、读取凭据或执行命令。
56
56
  你看到的内容可能缺失;缺失就写不知道,禁止编造。账本与审计在同一个用户可写目录,能力边界是防崩溃残留与误删,不防同用户蓄意篡改。
57
57
  不要复述开机卡,不要输出 JSON 之外的正文。`;
58
58
 
@@ -180,6 +180,17 @@ function relativeMinutes(iso, nowMs) {
180
180
  }
181
181
 
182
182
  const RECENT_ACTION_MS = 10 * 60 * 1000;
183
+ // R3.4: the stale flag must be visible in the shared hint, not only in
184
+ // board.json. Same signal as focus.stale:true — the hint never re-judges it.
185
+ const FOCUS_STALE_MINUTES = 30;
186
+
187
+ /** User-visible stale suffix for one focus row; '' when the goal is fresh. */
188
+ function focusStaleMarker(item, nowMs) {
189
+ if (!item || item.stale !== true) return '';
190
+ const age = relativeMinutes(item.updatedAt, nowMs);
191
+ const goalAge = age === null || age === '刚刚' ? '目标超 30 分钟未更新' : `目标 ${age}写入,超 ${FOCUS_STALE_MINUTES} 分钟未更新`;
192
+ return ` · stale(${goalAge})`;
193
+ }
183
194
 
184
195
  /**
185
196
  * L2 presence upgrade: sessions other than this one whose `lastAction.at` moved
@@ -238,6 +249,9 @@ export function renderHint(board, options = {}) {
238
249
  // Presence upgrade: 12-char prefix instead of a full opaque session id.
239
250
  const sessionId = sessionPrefix(item.sessionId, 12);
240
251
  let line = `- ${sessionId} 目标「${goal}」`;
252
+ // R3.4: stale goals must be visible to the other session, with the goal's
253
+ // own write time — never silently dropped from the shared hint.
254
+ line += focusStaleMarker(item, nowMs);
241
255
  const action = item.lastAction && typeof item.lastAction === 'object' ? item.lastAction : null;
242
256
  if (action) {
243
257
  const kind = neutralizeEnvelope(oneLine(action.kind, 24));
@@ -31,7 +31,7 @@ export const QUEUE_STATE_GLOSSARY = Object.freeze({
31
31
  queued: '待确认',
32
32
  approved: '已准待写',
33
33
  applying: '正在写',
34
- 'blocked-on-user': '请主人裁决',
34
+ 'blocked-on-user': '请主人判定',
35
35
  expired: '已过期(不计入队列)',
36
36
  });
37
37
 
@@ -607,8 +607,13 @@ export class RouterService {
607
607
  pulse: {
608
608
  ...(board.pulse ?? {}),
609
609
  active: board.activeSessions ?? 0,
610
- running: 0,
611
- blocked: this.panelState === 'blocked' ? 1 : 0,
610
+ // R4.2: same projection as board.json — never a second, hardcoded
611
+ // number. Before this, status.json said 0 while board.json said 34.
612
+ running: board.pulse?.running ?? 0,
613
+ // R4.7: blocked is the same pulse field as the board's — never the
614
+ // Router's own panelState wearing the same name with another value.
615
+ // Router state is already reported as status.state.
616
+ blocked: board.pulse?.blocked ?? 0,
612
617
  unknownSessions: board.unknownSessions ?? 0,
613
618
  queuedProposals: proposals.queue?.depth ?? 0,
614
619
  approvedProposals: proposalCounts.queued ?? 0,
@@ -1567,7 +1572,18 @@ export class RouterService {
1567
1572
  if (this.deps.installListener === false) return;
1568
1573
  if (typeof this.ctx.on !== 'function') return;
1569
1574
  try {
1570
- const disposer = this.ctx.on('session/event', (event) => this.handleSessionEvent(event), { prepend: false });
1575
+ // R4.4: the real host emits `session/event` as TWO positional args
1576
+ // `(session, event)` (recorded DSH logs, 2026-11-06), while several
1577
+ // shipped tests drive the legacy one-arg envelope. Accept both arities:
1578
+ // two-arg -> event is arg 2 and arg 1 is the session; one-arg -> arg 1 is
1579
+ // the event (which may still carry `.session` inside).
1580
+ const listener = (...args) => {
1581
+ const twoArg = args.length >= 2 && args[1] != null;
1582
+ const event = twoArg ? args[1] : args[0];
1583
+ const session = twoArg ? args[0] : null;
1584
+ return this.handleSessionEvent(event, session);
1585
+ };
1586
+ const disposer = this.ctx.on('session/event', listener, { prepend: false });
1571
1587
  if (typeof disposer === 'function') this.listenerDisposers.push(disposer);
1572
1588
  } catch (error) {
1573
1589
  this.#log('warn', `session/event listener install failed: ${errorMessage(error)}`);
@@ -1699,9 +1715,9 @@ export class RouterService {
1699
1715
  }
1700
1716
  }
1701
1717
 
1702
- #onTurnEnd(event, via) {
1718
+ #onTurnEnd(event, via, sessionArg = null) {
1703
1719
  if (this.disposed) return;
1704
- const session = this.#sessionOfEvent(event);
1720
+ const session = sessionArg ?? this.#sessionOfEvent(event);
1705
1721
  const id = sessionIdOf(session) ?? sessionIdOf(event);
1706
1722
  if (!id) return;
1707
1723
  const header = session?.header ?? {};
@@ -1740,20 +1756,20 @@ export class RouterService {
1740
1756
  }
1741
1757
  }
1742
1758
 
1743
- #handleLiveEvent(event) {
1759
+ #handleLiveEvent(event, sessionArg = null) {
1744
1760
  const type = event?.type;
1745
1761
  if (type === 'session/created') {
1746
- this.#onSessionCreated(this.#sessionOfEvent(event), 'session/event');
1762
+ this.#onSessionCreated(sessionArg ?? this.#sessionOfEvent(event), 'session/event');
1747
1763
  return true;
1748
1764
  }
1749
1765
  if (type === 'turn/end') {
1750
- this.#onTurnEnd(event, 'turn/end');
1766
+ this.#onTurnEnd(event, 'turn/end', sessionArg);
1751
1767
  return true;
1752
1768
  }
1753
1769
  if (type === 'agent/status') {
1754
1770
  const status = event?.data?.state ?? event?.data?.status ?? event?.status;
1755
1771
  if (status === 'idle') {
1756
- this.#onTurnEnd(event, 'agent/status');
1772
+ this.#onTurnEnd(event, 'agent/status', sessionArg);
1757
1773
  return true;
1758
1774
  }
1759
1775
  }
@@ -1764,10 +1780,10 @@ export class RouterService {
1764
1780
  * turn/end 唤醒:只认 reason.kind === 'completed',2 秒防抖 + 5 分钟合并。
1765
1781
  * 返回 {scheduled, coalesced?, debounced?, ignored?},方便单测。
1766
1782
  */
1767
- handleSessionEvent(event) {
1783
+ handleSessionEvent(event, session = null) {
1768
1784
  // L1 heartbeat runs for every turn boundary, independent of reflectOnTurn
1769
1785
  // and of reason.kind (an aborted turn still ends).
1770
- try { this.#handleLiveEvent(event); } catch (error) {
1786
+ try { this.#handleLiveEvent(event, session); } catch (error) {
1771
1787
  this.#log('warn', `live heartbeat failed: ${errorMessage(error)}`);
1772
1788
  }
1773
1789
  const type = event?.type;
@@ -15,13 +15,14 @@
15
15
  * 硬约束(代码结构保证,不是开关):
16
16
  * - 没有 exec/spawn/child_process/http/fetch 任何引用;
17
17
  * - 唯一的写操作 appendJsonl(),目标被限制在 outDir 之内;
18
- * - 不读 engram / profile / <memory-root>,ctx 只来自调用方注入。
18
+ * - 不读外部记忆库 / profile / <memory-root>,ctx 只来自调用方注入。
19
19
  *
20
20
  * 用法见 SHADOW-RUN.md;`node src/router/shadow.mjs --help`。
21
21
  */
22
22
 
23
23
  import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
24
24
  import path from 'node:path';
25
+ import { pluginEvidencePrefixes } from '../adapters/registry.js';
25
26
  import { fileURLToPath } from 'node:url';
26
27
 
27
28
  import { RULES, route } from './route.js';
@@ -357,7 +358,7 @@ export function validWriteScope(scope) {
357
358
  if (value.startsWith('/') || value.startsWith('\\')) return false;
358
359
  if (value.includes('..')) return false;
359
360
  if (value.includes('~')) return false;
360
- const prefixes = ['memory/', 'engram:', 'taskboard:', 'sandbox:', 'evidence:', 'fixtures:'];
361
+ const prefixes = ['memory/', ...pluginEvidencePrefixes(), 'taskboard:', 'sandbox:', 'evidence:', 'fixtures:'];
361
362
  return prefixes.some((prefix) => value.startsWith(prefix));
362
363
  }
363
364
 
@@ -668,7 +669,7 @@ const HELP = `R1 影子模式(只记录,不执行)
668
669
  --json 机器可读输出
669
670
  --help
670
671
 
671
- 本命令没有任何执行分支:不会调工具、不会写记忆库、不会碰 engram/profile。
672
+ 本命令没有任何执行分支:不会调工具、不会写记忆库、不会碰外部记忆库/profile。
672
673
  `;
673
674
 
674
675
  function parseArgs(argv) {
@@ -108,7 +108,7 @@ const PHASES = new Set(['prepare', 'backup', 'cut', 'restart', 'receipt-pending'
108
108
  // broken launcher) can edit, and they used to be interpolated into conversation
109
109
  // text verbatim. Only the fixed phase enum and a [a-z0-9-] token may be echoed
110
110
  // (a legal session id in full, per R4-02); everything else is replaced character
111
- // by character, so a value like `..\..\C:\Users\x` can never look like a path in
111
+ // by character, so a value like `..\..\<user-dir>\\x` can never look like a path in
112
112
  // the receipt.
113
113
  const SAFE_TOKEN_RE = /^[a-z0-9-]+$/;
114
114
 
@@ -1079,7 +1079,7 @@ export function receiptText(report, { appendingReceipt = false, seq = null } = {
1079
1079
  // R3-04 / R4-02: only a [a-z0-9-] token may be echoed. A legal session id is
1080
1080
  // shown in full (operators match UUIDs, not their first 8 chars); a value
1081
1081
  // that fails the whitelist is still sanitised and shortened. `asText` alone
1082
- // let a tampered state put `..\..\C:\Users\...` into the transcript.
1082
+ // let a tampered state put `..\..\<user-dir>\\...` into the transcript.
1083
1083
  const sessionId = displaySessionId(asText(report?.sessionId) ?? asText(checks.sessionId));
1084
1084
  if (sessionId) lines.push(`会话 id:${sessionId}`);
1085
1085
 
@@ -34,7 +34,7 @@ import { archiveFileName, textOfParts } from './original.js';
34
34
  import { privacyScan } from './recall/privacy.mjs';
35
35
 
36
36
  export const D2_DEFAULTS = Object.freeze({
37
- enabled: false,
37
+ enabled: true,
38
38
  maxInlineChars: 4096,
39
39
  headRatio: 0.6,
40
40
  skipTools: Object.freeze(['read']),
@@ -70,7 +70,7 @@ export function normalizeD2Config(raw) {
70
70
  ? source.skipTools.filter((name) => typeof name === 'string' && name !== '')
71
71
  : [...D2_DEFAULTS.skipTools];
72
72
  return {
73
- enabled: source.enabled === true,
73
+ enabled: source.enabled !== false,
74
74
  maxInlineChars: Math.round(clampNumber(
75
75
  source.maxInlineChars,
76
76
  D2_DEFAULTS.maxInlineChars,