@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * dsh-auto-recall — 插件骨架(**未安装**,见 ../README.md 与 WIRING.md §9)。
3
3
  *
4
- * 与 WIRING.md §3 的差异(红队第三轮 mustFix 已修):
4
+ * 与 WIRING.md §3 的差异(评审第三轮 mustFix 已修):
5
5
  * - 注册用显式 `{ prepend: false }`,不跟 identity-router 抢外层;
6
6
  * identity-router 在 `next()` 之前把开机卡插到第一条真实 user 之后,auto 作为内层
7
7
  * 一定看得到它,最终顺序固定为 `[user, boot, runtime, auto]`(真实 Cordis
@@ -10,7 +10,7 @@
10
10
  * - firstUserText 只认真正的用户消息:`session/event` 里跳过 `【开机卡】`/`【自动想起】`
11
11
  * 注入(它们不再是 kind:'user',但历史消息仍可能带标记),否则开机卡会污染之后
12
12
  * 每一轮的查询词;第 1 轮内后来到达的真人消息覆盖先前候选,防外部插件用
13
- * kind:'user' 把文本插到真实提问之前(红队 ADV1);
13
+ * kind:'user' 把文本插到真实提问之前(评审 ADV1);
14
14
  * - 注入前过 privacyScan(privacy-gate 的 scanPrivacy);标题、摘要、路径都扫,
15
15
  * 路径命中整卡丢弃;
16
16
  * - identity-router 已注入的卡路径进 seenPaths(路径级排除),不再无条件排除
@@ -23,6 +23,13 @@
23
23
  */
24
24
 
25
25
  import { loadCards } from './cards.mjs';
26
+ import { sharedVisibility } from './shared-visibility.js';
27
+ import { extractTimeRefs } from './time-normalizer.mjs';
28
+ import { readEntityAliases, resolveEntityAliases, expandQueryWithEntities } from './entities.mjs';
29
+ import { resolveAnaphora, expandQueryWithAnaphora } from './anaphora.mjs';
30
+ import { appendAuditRecord } from '../memory/audit-records.js';
31
+ import { buildEvidencePackage, writeEvidencePackage } from '../memory/evidence.js';
32
+ import { fastAnswerFromRecallItems } from '../memory/recall-v2.js';
26
33
  import { buildQueryText, chooseRecall, decideTrigger, renderInjection } from './recall-core.mjs';
27
34
  import { createEmbeddingRuntime } from './embedding.mjs';
28
35
  import { privacyScan } from './privacy.mjs';
@@ -34,10 +41,13 @@ import {
34
41
  isRealUserMessage,
35
42
  mergeInjectionOrder,
36
43
  } from './inject-order.mjs';
37
- import { appendFileSync, mkdirSync } from 'node:fs';
44
+ import { appendFileSync, mkdirSync, readdirSync, statSync } from 'node:fs';
38
45
  import { dirname, join } from 'node:path';
39
46
  import { dshHome } from '../home.js';
40
47
  import { DEFAULT_MEMORY_DIRNAME } from '../memory/paths.js';
48
+ import { startPrestepPhase, notePrestepDegrade, setPrestepMeta } from '../metrics/prestep-latency.mjs';
49
+ import { readVerifiedAutoCards } from '../memory/auto-cards.js';
50
+ import { sampleContentHash } from '../memory/auto-cards.js';
41
51
 
42
52
  export const name = 'dsh-auto-recall';
43
53
  export const inject = [];
@@ -72,12 +82,13 @@ const DEFAULTS = Object.freeze({
72
82
  privacyPolicy: 'drop',
73
83
  skipSubagents: true,
74
84
  log: false,
75
- embedding: { enabled: false },
85
+ embedding: { enabled: true },
86
+ evidencePackage: true,
76
87
  });
77
88
 
78
89
  function normalize(config = {}) {
79
90
  const cfg = { ...DEFAULTS, ...config };
80
- // 红队 M1:空 / 未配置时走可移植默认链,绝不落到构建机的绝对路径。
91
+ // 评审 M1:空 / 未配置时走可移植默认链,绝不落到构建机的绝对路径。
81
92
  const root = typeof cfg.memoryRoot === 'string' ? cfg.memoryRoot.trim() : '';
82
93
  cfg.memoryRoot = root === '' ? defaultMemoryRoot() : root;
83
94
  // 注入硬上限 2K token/轮(A4);下限保留 200,避免误配把注入挤成一条。
@@ -90,7 +101,11 @@ function normalize(config = {}) {
90
101
  // 测试/嵌入时可注入消息工厂,避免依赖 @deepseek-ai/dsh-llm 的解析环境。
91
102
  cfg.createUserMessage = typeof config.createUserMessage === 'function' ? config.createUserMessage : null;
92
103
  // embedding 由 embedding.mjs 自己归一化(enabled 默认 false、modelPath 必填)。
93
- cfg.embedding = config.embedding && typeof config.embedding === 'object' ? { ...config.embedding } : { enabled: false };
104
+ cfg.embedding = config.embedding && typeof config.embedding === 'object' ? { ...config.embedding } : { enabled: true };
105
+ cfg.evidencePackage = cfg.evidencePackage !== false;
106
+ cfg.evidenceAuditKey = config.evidenceAuditKey && (Buffer.isBuffer(config.evidenceAuditKey) || typeof config.evidenceAuditKey === 'string')
107
+ ? config.evidenceAuditKey
108
+ : null;
94
109
  return cfg;
95
110
  }
96
111
 
@@ -168,19 +183,57 @@ export function apply(ctx, config = {}) {
168
183
  }
169
184
  };
170
185
 
186
+ // B2: entity alias registry is read lazily (missing file => no-op, no latency).
187
+ let entityAliasesCache = null;
188
+ const getEntityAliases = () => {
189
+ if (entityAliasesCache !== null) return entityAliasesCache;
190
+ try {
191
+ entityAliasesCache = readEntityAliases({ memoryRoot: cfg.memoryRoot });
192
+ } catch {
193
+ entityAliasesCache = { present: false, aliases: {} };
194
+ }
195
+ return entityAliasesCache;
196
+ };
197
+
171
198
  const states = new Map();
172
199
  const stateFor = (key) => {
173
200
  let st = states.get(key);
174
201
  if (!st) {
175
- st = { turn: 0, seen: new Set(), lastQuery: '', compacted: false, warned: false, firstUserText: '' };
202
+ st = { turn: 0, seen: new Set(), lastQuery: '', compacted: false, warned: false, firstUserText: '', sharedInjected: false };
176
203
  states.set(key, st);
177
204
  }
178
205
  return st;
179
206
  };
180
207
 
208
+ /** Cheap mtime/size signature so new or edited cards are visible next turn. */
209
+ const cardSignature = () => {
210
+ const parts = [];
211
+ const walk = (dir) => {
212
+ let entries = [];
213
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
214
+ for (const entry of entries) {
215
+ const full = join(dir, entry.name);
216
+ if (entry.isDirectory()) walk(full);
217
+ else if (entry.isFile() && /\.md$/i.test(entry.name)) {
218
+ try {
219
+ const st = statSync(full);
220
+ parts.push(`${full}:${st.mtimeMs}:${st.size}:${sampleContentHash(full, st.size)}`);
221
+ } catch { /* unreadable card is not part of the signature */ }
222
+ }
223
+ }
224
+ };
225
+ for (const dir of cfg.dirs) walk(join(cfg.memoryRoot, dir));
226
+ parts.sort();
227
+ return parts.join('|');
228
+ };
229
+
181
230
  let cards = null;
231
+ let cardsSignature = '';
182
232
  const getCards = () => {
183
- if (cards) return cards;
233
+ const signature = cardSignature();
234
+ if (cards && signature === cardsSignature) return cards;
235
+ cardsSignature = signature;
236
+ cards = null;
184
237
  try {
185
238
  cards = loadCards(cfg.memoryRoot, { dirs: cfg.dirs });
186
239
  } catch (error) {
@@ -239,13 +292,55 @@ export function apply(ctx, config = {}) {
239
292
  const st = stateFor(key);
240
293
  const messages = Array.isArray(decision.messages) ? decision.messages : [];
241
294
  const turn = Number.isFinite(Number(payload?.turn)) ? Number(payload.turn) : st.turn + 1;
242
-
295
+ // M2-C R1–R6: shared visibility block is injected on the FIRST turn only.
296
+ const rawSharedNow = payload?.at ?? payload?.eventTime ?? payload?.now ?? null;
297
+ const sharedNow = rawSharedNow === null || rawSharedNow === undefined
298
+ ? Date.now()
299
+ : (Number.isFinite(Number(rawSharedNow)) ? Number(rawSharedNow) : (Date.parse(String(rawSharedNow)) || Date.now()));
300
+ let sharedMessage = null;
301
+ let sharedVisibilityResult = null;
302
+ const attachShared = (value) => (sharedMessage === null ? value : { ...value, messages: mergeInjectionOrder(Array.isArray(value?.messages) ? value.messages : messages, sharedMessage) });
303
+
304
+ // B3: recall phase wraps the whole recall decision/injection (timing only).
305
+ const recallPhase = startPrestepPhase(key, turn, 'recall');
243
306
  try {
307
+ if (turn === 1 && st.sharedInjected !== true && cfg.memoryRoot) {
308
+ try {
309
+ const visibility = sharedVisibility({
310
+ memoryRoot: cfg.memoryRoot,
311
+ currentSessionId: payload?.agent?.session?.id ?? key,
312
+ now: sharedNow,
313
+ privacyScan,
314
+ });
315
+ if (visibility && visibility.block) {
316
+ const factory = await getCreateUserMessage();
317
+ if (factory) {
318
+ sharedMessage = assertInjectionMessage(factory({ content: [{ type: 'text', text: visibility.block }], source: injectionSource() }));
319
+ sharedVisibilityResult = { count: visibility.count, chars: visibility.block.length, sessions: visibility.sessions.map((session) => session.sessionId) };
320
+ st.sharedInjected = true;
321
+ }
322
+ }
323
+ } catch { /* shared visibility must never block the turn */ }
324
+ }
325
+ const recentTexts = userTexts(messages).slice(-2);
244
326
  const queryText = buildQueryText({
245
327
  firstUserText: st.firstUserText,
246
- recentTexts: userTexts(messages).slice(-2),
328
+ recentTexts,
247
329
  cwd: process.cwd(),
248
330
  });
331
+ // B2: entity aliases + anaphora expansion run before retrieval (read-only channels).
332
+ const aliases = getEntityAliases();
333
+ const aliasResolution = aliases.present ? resolveEntityAliases(queryText, aliases.aliases) : { terms: [], matches: [] };
334
+ const entityExpanded = expandQueryWithEntities(queryText, aliasResolution);
335
+ const anaphora = resolveAnaphora({ text: queryText, recentTexts });
336
+ const anaphoraExpanded = expandQueryWithAnaphora(entityExpanded.text, anaphora);
337
+ const expandedQuery = anaphoraExpanded.text;
338
+ // Event time only when the host/pre-step provides it; never Date.now() here.
339
+ const rawEventNow = payload?.at ?? payload?.eventTime ?? payload?.now ?? null;
340
+ const eventNow = rawEventNow === null || rawEventNow === undefined
341
+ ? null
342
+ : (Number.isFinite(Number(rawEventNow)) ? Number(rawEventNow) : (Date.parse(String(rawEventNow)) || null));
343
+ const queryTimeRefs = eventNow === null ? [] : extractTimeRefs(queryText, { now: eventNow });
249
344
 
250
345
  const gate = decideTrigger({
251
346
  step: Number(payload?.step),
@@ -255,12 +350,30 @@ export function apply(ctx, config = {}) {
255
350
  options: { everyNTurns: cfg.everyNTurns },
256
351
  });
257
352
  if (!gate.recall) {
353
+ notePrestepDegrade(key, turn, { kind: 'gate-skip', phase: 'recall', detail: gate.why });
258
354
  logRecall(key, turn, { items: [], rejections: [] });
259
355
  observe({ key, turn, why: gate.why, queryChars: queryText.length, tokens: 0, ms: Date.now() - started });
260
- return decision;
356
+ return attachShared(decision);
261
357
  }
262
358
 
263
359
  const loaded = getCards();
360
+ // 1.8.0 AutoCards: JSONL event→index cards are recall candidates too, so a
361
+ // matched card carries its origin anchor into the injected block.
362
+ let loadedForRecall = loaded;
363
+ if (cfg.memoryRoot) {
364
+ try {
365
+ const autoCards = readVerifiedAutoCards({ memoryRoot: cfg.memoryRoot }).map((card) => ({
366
+ path: `auto:${card.cardId}`,
367
+ title: card.title,
368
+ body: [card.title, Array.isArray(card.terms) ? card.terms.join(' ') : ''].filter(Boolean).join('\n'),
369
+ scopeIn: [],
370
+ aliases: Array.isArray(card.terms) ? card.terms : [],
371
+ cardId: card.cardId,
372
+ anchor: card.anchor,
373
+ }));
374
+ if (autoCards.length > 0) loadedForRecall = [...loaded, ...autoCards];
375
+ } catch { /* cards are additive; never block markdown recall */ }
376
+ }
264
377
  // 路径级排除:identity-router 本轮已经注入过的卡,不再注入第二遍。
265
378
  // 不再无条件排除首轮 handover/:没装 identity-router 时那张卡就是唯一开机卡。
266
379
  const seen = [...new Set([
@@ -274,25 +387,29 @@ export function apply(ctx, config = {}) {
274
387
  let vectors = null;
275
388
  if (embedding.enabled) {
276
389
  try {
277
- const ready = await embedding.ensure(loaded);
390
+ const ready = await embedding.ensure(loaded, { signature: cardsSignature });
278
391
  if (ready.ok) {
279
392
  queryVector = await embedding.embedQuery(queryText);
280
393
  vectors = ready.cardVectors;
281
394
  } else if (!embeddingWarned) {
282
395
  embeddingWarned = true;
396
+ notePrestepDegrade(key, turn, { kind: 'embedding-fallback', phase: 'recall', detail: ready.code ?? 'not-ready' });
283
397
  warn('EMBEDDING_FALLBACK', ready.code, ready.message);
284
398
  }
285
399
  } catch (error) {
286
400
  if (!embeddingWarned) {
287
401
  embeddingWarned = true;
402
+ notePrestepDegrade(key, turn, { kind: 'embedding-fallback', phase: 'recall', detail: String(error?.code ?? error?.message ?? error) });
288
403
  warn('EMBEDDING_FALLBACK', error?.code ?? error?.message ?? error);
289
404
  }
290
405
  }
291
406
  }
292
407
 
293
- const result = chooseRecall({
294
- queryText,
295
- cards: loaded,
408
+ // B3: budget phase = ranking + budget trimming inside chooseRecall.
409
+ const budgetPhase = startPrestepPhase(key, turn, 'budget');
410
+ const recallInput = {
411
+ queryText: expandedQuery,
412
+ cards: loadedForRecall,
296
413
  queryVector,
297
414
  vectors,
298
415
  budget: cfg.budget,
@@ -304,6 +421,9 @@ export function apply(ctx, config = {}) {
304
421
  privacyPolicy: cfg.privacyPolicy,
305
422
  privacyScan,
306
423
  trace: true,
424
+ // B2 channels: neutral when empty (legacy ordering unchanged).
425
+ queryTimeRefs,
426
+ entityAliases: aliases.present ? aliases.aliases : undefined,
307
427
  fusion: {
308
428
  keywordWeight: embedding.config.keywordWeight,
309
429
  vectorWeight: embedding.config.vectorWeight,
@@ -311,9 +431,97 @@ export function apply(ctx, config = {}) {
311
431
  minFusedScore: embedding.config.minFusedScore,
312
432
  },
313
433
  },
434
+ };
435
+ let result = chooseRecall(recallInput);
436
+ budgetPhase.end();
437
+ // 1.8.0 two-layer quota: at most 2 curated cards + 1 AutoCard per turn;
438
+ // curated hits always render first (tier weight 1.12 only affects order
439
+ // inside the curated layer, so existing output stays stable).
440
+ if (Array.isArray(result.items) && result.items.length > 0) {
441
+ const isAuto = (item) => String(item?.path ?? '').startsWith('auto:');
442
+ let curatedItems = result.items.filter((item) => !isAuto(item))
443
+ .sort((a, b) => (b.score * 1.12) - (a.score * 1.12));
444
+ const eventItems = result.items.filter(isAuto).slice(0, 1);
445
+ // Curated floor: event cards may never squeeze curated cards out.
446
+ if (curatedItems.length === 0 && loaded.length > 0) {
447
+ const curatedOnly = chooseRecall({ ...recallInput, cards: loaded });
448
+ if (curatedOnly.items.length > 0) {
449
+ result = curatedOnly;
450
+ curatedItems = curatedOnly.items.slice().sort((a, b) => (b.score * 1.12) - (a.score * 1.12));
451
+ }
452
+ }
453
+ result.items = [...curatedItems.slice(0, 2), ...eventItems].slice(0, cfg.maxItems);
454
+ }
455
+ // M2-C #6: every recall decision atomically publishes .longlive/evidence/latest.json.
456
+ let evidencePackage = null;
457
+ if (cfg.evidencePackage && cfg.memoryRoot) {
458
+ try {
459
+ const nowIso = new Date().toISOString();
460
+ const fast = fastAnswerFromRecallItems({ question: queryText, items: result.items });
461
+ const pkg = buildEvidencePackage({
462
+ sessionId: payload?.agent?.session?.id ?? key,
463
+ turn,
464
+ query: queryText,
465
+ timings: {
466
+ questionAt: eventNow === null ? null : new Date(eventNow).toISOString(),
467
+ queueMs: 0,
468
+ prefetchAt: new Date(started).toISOString(),
469
+ retrievedAt: nowIso,
470
+ evidenceReadyAt: nowIso,
471
+ },
472
+ anchors: result.items.map((item) => ({
473
+ path: item.path,
474
+ excerpt: `${item.title ?? ''} — ${item.excerpt ?? ''}`.trim(),
475
+ score: Number.isFinite(Number(item.score)) ? Number(item.score) : null,
476
+ l0Status: 'card',
477
+ })),
478
+ status: result.items.length > 0 ? 'ready' : 'no_evidence',
479
+ fastAnswer: fast.fastAnswer === null ? null : { text: fast.fastAnswer.text, valueType: fast.fastAnswer.valueType, anchor: fast.fastAnswer.anchor },
480
+ fallback: { used: false, reason: result.items.length > 0 ? null : 'zero-hit' },
481
+ });
482
+ const written = writeEvidencePackage({ memoryRoot: cfg.memoryRoot, pkg, auditKey: cfg.evidenceAuditKey });
483
+ evidencePackage = { queryId: pkg.queryId, status: pkg.status, anchors: pkg.anchors.length, fastAnswer: pkg.fastAnswer !== null, packageSha256: written.packageSha256 };
484
+ } catch (error) {
485
+ // Write failure must never block injection.
486
+ evidencePackage = { error: error?.code ?? 'E_EVIDENCE_WRITE' };
487
+ notePrestepDegrade(key, turn, { kind: 'evidence-write', phase: 'recall', detail: String(error?.code ?? error?.message ?? error) });
488
+ warn('EVIDENCE_WRITE_FAILED', error);
489
+ }
490
+ }
491
+ if (Number(result?.hits?.droppedByBudget ?? 0) > 0) {
492
+ notePrestepDegrade(key, turn, { kind: 'budget-trim', phase: 'budget', detail: `dropped=${result.hits.droppedByBudget}` });
493
+ }
494
+ setPrestepMeta(key, turn, {
495
+ budget: cfg.budget,
496
+ eligible: Number(result?.hits?.eligible ?? 0),
497
+ tokens: Number(result?.tokens ?? 0),
314
498
  });
315
499
 
500
+ // A5 unified audit: every real recall decision lands one .longlive/audit/records.jsonl row.
501
+ try {
502
+ appendAuditRecord({
503
+ memoryRoot: cfg.memoryRoot,
504
+ action: 'recall',
505
+ recordId: result.items.length > 0 ? result.items.map((item) => item.path).join(',') : null,
506
+ readerTier: 'host',
507
+ questionId: 'unknown',
508
+ expectedTurns: null,
509
+ answerText: null,
510
+ extra: {
511
+ turn,
512
+ queryChars: queryText.length,
513
+ expanded: expandedQuery !== queryText,
514
+ expansion: { entityTerms: aliasResolution.terms ?? [], anaphoraTerms: anaphora.terms ?? [], timeRefs: queryTimeRefs.length },
515
+ sharedVisibility: sharedVisibilityResult,
516
+ evidencePackage,
517
+ eligible: result.hits.eligible,
518
+ injected: result.items.map((item) => item.path),
519
+ tokens: result.tokens,
520
+ },
521
+ });
522
+ } catch { /* recall auditing must never block injection */ }
316
523
  if (result.items.length === 0) {
524
+ notePrestepDegrade(key, turn, { kind: 'zero-hit', phase: 'recall', detail: `eligible=${result.hits.eligible}` });
317
525
  st.lastQuery = queryText;
318
526
  st.compacted = false;
319
527
  logRecall(key, turn, result);
@@ -328,12 +536,16 @@ export function apply(ctx, config = {}) {
328
536
  privacy: result.hits.droppedByPrivacy,
329
537
  },
330
538
  });
331
- return decision; // 无命中:什么都不注入
539
+ return attachShared(decision); // 无命中:什么都不注入(共享可见块仍可注入)
332
540
  }
333
541
 
542
+ // B3: inject phase = render + message factory + merge into decision.messages.
543
+ const injectPhase = startPrestepPhase(key, turn, 'inject');
334
544
  const text = renderInjection(result, { privacyScan });
335
545
  const factory = await getCreateUserMessage();
336
546
  if (!factory) {
547
+ injectPhase.end();
548
+ notePrestepDegrade(key, turn, { kind: 'factory-unavailable', phase: 'inject' });
337
549
  logRecall(key, turn, { items: [], rejections: result.rejections });
338
550
  return decision;
339
551
  }
@@ -348,6 +560,7 @@ export function apply(ctx, config = {}) {
348
560
  for (const item of result.items) st.seen.add(item.path);
349
561
  st.lastQuery = queryText;
350
562
  st.compacted = false;
563
+ injectPhase.end();
351
564
 
352
565
  logRecall(key, turn, result);
353
566
  observe({
@@ -363,15 +576,18 @@ export function apply(ctx, config = {}) {
363
576
  },
364
577
  });
365
578
 
366
- return { ...decision, messages: mergeInjectionOrder(messages, message) };
579
+ return { ...decision, messages: mergeInjectionOrder(sharedMessage === null ? messages : mergeInjectionOrder(messages, sharedMessage), message) };
367
580
  } catch (error) {
368
581
  if (!st.warned) {
369
582
  st.warned = true;
370
583
  warn('RECALL_FAILED', error?.code ?? error?.message ?? error);
371
584
  }
585
+ notePrestepDegrade(key, turn, { kind: 'error', phase: 'recall', detail: String(error?.code ?? error?.message ?? error) });
372
586
  observe({ key, turn, why: 'error', error: String(error?.code ?? error?.message ?? error), tokens: 0 });
373
587
  logRecall(key, turn, { items: [], rejections: [] });
374
- return decision; // 插件失败不许拖垮本轮
588
+ return attachShared(decision); // 插件失败不许拖垮本轮
589
+ } finally {
590
+ try { recallPhase.end(); } catch { /* timing only */ }
375
591
  }
376
592
  }, { prepend: false });
377
593
 
@@ -381,7 +597,7 @@ export function apply(ctx, config = {}) {
381
597
  const st = stateFor(session?.id ?? 'unknown');
382
598
  if (event?.type === 'user/message' && isRealUserMessage(event?.data)) {
383
599
  const text = firstTextOf(event?.data);
384
- // 红队第三轮 mustFix #1:注入的【开机卡】/【自动想起】也是 user/message,
600
+ // 评审第三轮 mustFix #1:注入的【开机卡】/【自动想起】也是 user/message,
385
601
  // 旧代码只看 type 就把它们当成 firstUserText,导致之后每轮 query 都混进开机卡。
386
602
  //
387
603
  // v4 / ADV1:外部插件仍可能用 kind:'user' 把一段上下文插到真实提问**之前**。
@@ -12,12 +12,12 @@
12
12
  *
13
13
  * 开机卡插在**第一条真实 user 消息之后**,不再 unshift 到最前:宿主 TurnNavigator
14
14
  * 取该轮第一个可见 `kind:'user'` 节点做 tooltip 的 prompt(client.js:8373-8384),
15
- * boot 在最前会让第 1 轮显示【开机卡】而不是用户提问(红队第三轮 mustFix #2)。
15
+ * boot 在最前会让第 1 轮显示【开机卡】而不是用户提问(评审第三轮 mustFix #2)。
16
16
  *
17
17
  * auto 只负责把自己 append 到最后;不再把消息插到 boot 后面(那会得到
18
- * `[boot, auto, user, runtime]`,红队 S2 的顺序问题)。
18
+ * `[boot, auto, user, runtime]`,评审 S2 的顺序问题)。
19
19
  *
20
- * v4 / 红队第四轮 M1(2026-11-04):注入消息的 source.kind 从 'user' 改为
20
+ * v4 / 评审第四轮 M1(2026-11-04):注入消息的 source.kind 从 'user' 改为
21
21
  * 'session-reference',形状固定为
22
22
  *
23
23
  * { kind:'session-reference', form:'recall', version:1, references:[] }
@@ -35,9 +35,9 @@
35
35
  *
36
36
  * 识别契约:
37
37
  * - 本插件 / identity-router 的注入靠 kind 直接识别(不再依赖正文标记);
38
- * - 正文标记(【自动想起】/【开机卡】)继续保留,用于识别历史消息与外部 engram
38
+ * - 正文标记(【自动想起】/【开机卡】)继续保留,用于识别历史消息与外部记忆注入
39
39
  * 风格的注入,also keeps firstUserText from eating a legacy card;
40
- * - **外部插件**仍可能用 kind:'user' 把文本插到真实提问前(红队 ADV1)。识别不了
40
+ * - **外部插件**仍可能用 kind:'user' 把文本插到真实提问前(评审 ADV1)。识别不了
41
41
  * 任意纯文本,所以 firstUserText 的取值在 index.js 里按「第 1 轮最后一条真人
42
42
  * 消息」锁定,isRealUserMessage 只负责排掉已知注入形态。
43
43
  */
@@ -47,6 +47,8 @@
47
47
  * 注入只允许白名单内的 kind;坏 kind 会在 append 时被本插件拒绝,避免日志被
48
48
  * 毒化(append 接受任意非空字符串,但 adoptSessionEvent 会抛 invalid source)。
49
49
  */
50
+ import { isExternalMemoryKind } from '../adapters/constants.js';
51
+
50
52
  export const HOST_SOURCE_KINDS = Object.freeze([
51
53
  'user', 'model', 'tool', 'system-prompt', 'model-selection', 'runtime-context',
52
54
  'working-directory', 'user-approval', 'ptc-mode', 'tool-registry', 'user-rpc',
@@ -153,7 +155,7 @@ export function isAutoMessage(message) {
153
155
  * 且不是本插件 / identity-router / 已知外部注入形态。
154
156
  *
155
157
  * v4 起注入用 kind:'session-reference',kind 本身就能挡掉;这里再按正文特征
156
- * 排一层(红队 ADV1):带 `source.form` / `source.sections` 的、首行是已知标记的,
158
+ * 排一层(评审 ADV1):带 `source.form` / `source.sections` 的、首行是已知标记的,
157
159
  * 都不算真人消息。无法凭正文区分「任意外部插件写的纯文本 kind:user」——那类消息
158
160
  * 由 firstUserText 的「第 1 轮最后一条」规则处理(见 index.js)。
159
161
  * @param {object} message
@@ -181,7 +183,7 @@ export function textsOf(message) {
181
183
  }
182
184
 
183
185
  /**
184
- * 本轮已经注入的文本:identity 开机卡 + 旧 plugin:* / engram 消息。
186
+ * 本轮已经注入的文本:identity 开机卡 + 旧 plugin:* / 外部记忆注入消息。
185
187
  * 给 chooseRecall 做文本级去重;kind 合法化成 session-reference 后不能只靠 kind 判断。
186
188
  */
187
189
  export function injectedTexts(messages) {
@@ -192,7 +194,7 @@ export function injectedTexts(messages) {
192
194
  continue;
193
195
  }
194
196
  const kind = sourceKindOf(message);
195
- if (!kind.startsWith('plugin:') && !kind.includes('engram')) continue;
197
+ if (!kind.startsWith('plugin:') && !isExternalMemoryKind(kind)) continue;
196
198
  out.push(...textsOf(message));
197
199
  }
198
200
  return out;
@@ -13,7 +13,7 @@
13
13
  * 命中口径 = 隐私门的 verdict==='local_only' **或** 核心兜底正则命中(取并集,宁可多拦)。
14
14
  * 兜底分两类:
15
15
  * - PII:手机号 / 邮箱 / 身份证(1.6.7 起就有);
16
- * - 密钥/令牌(1.6.8 红队 S3 补):sk-*、Bearer …、*_API_KEY=*、token=/secret=/password=、
16
+ * - 密钥/令牌(1.6.8 评审 S3 补):sk-*、Bearer …、*_API_KEY=*、token=/secret=/password=、
17
17
  * 私钥头、AWS/GitHub/Slack 凭证前缀。活动板是跨会话共享的,
18
18
  * 手机号只泄漏一个人的联系方式,密钥泄漏的是账号本身。
19
19
  *