@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
@@ -2,7 +2,7 @@
2
2
  * auto-recall v0 — 「自动想起」的纯函数核心。
3
3
  *
4
4
  * 设计约束(见 ../../DESIGN.md):
5
- * - 本文件不 import 任何东西(无 fs / 无网络 / 无宿主 API),可直接单测;
5
+ * - 不 import fs/网络/宿主 API,可直接单测;唯一依赖是纯函数 trust 公式(A3 排序权重);
6
6
  * - 打分算法与 scope-route 原型对齐(词袋子串命中 + 长度权重),
7
7
  * 以便沿用它在金标 26/27、留出集 9/10 上的既有结论;
8
8
  * - 没命中 / 低于阈值 / 超预算 → 不注入(items: []),绝不「猜一个最像的」;
@@ -18,6 +18,10 @@
18
18
  * defaultPrivacyScan(text) -> {verdict, spans, ...} // 无外部依赖的 PII 兜底
19
19
  */
20
20
 
21
+ import { trustScoreAt } from '../memory/trust.js';
22
+ import { cardTimeAffinity } from './time-normalizer.mjs';
23
+ import { resolveEntityAliases } from './entities.mjs';
24
+
21
25
  export const VERSION = 'auto-recall/0.1.0';
22
26
 
23
27
  export const DEFAULTS = Object.freeze({
@@ -38,6 +42,29 @@ export const DEFAULTS = Object.freeze({
38
42
  /** 隐私命中且 privacyPolicy='title' 时,正文位置显示这个标记。 */
39
43
  export const PRIVACY_HIDDEN_MARK = '[已隐藏]';
40
44
 
45
+ /**
46
+ * Render card-controlled strings as inert data: no newlines/control chars, no
47
+ * bidi overrides, collapsed whitespace, bounded length. The template around it
48
+ * is fixed; this function is the only path for title/body/path text.
49
+ */
50
+ export function sanitizeDataText(value, max = 400) {
51
+ const text = String(value ?? '')
52
+ .replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f\uFEFF]/gu, ' ')
53
+ .replace(/\s+/gu, ' ')
54
+ .trim();
55
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
56
+ }
57
+
58
+ /** Safe anchor origin line: generated only from validated anchor fields. */
59
+ export function renderAnchorDataLine(anchor) {
60
+ if (!anchor || typeof anchor.sessionId !== 'string' || !Number.isInteger(anchor.turn) || anchor.turn < 0) return '';
61
+ const suffix = `#${anchor.turn}]`;
62
+ const prefix = '[原文 ';
63
+ const maxSession = Math.max(1, 60 - prefix.length - suffix.length);
64
+ const session = anchor.sessionId.replace(/[^A-Za-z0-9._:-]/gu, '_').slice(0, maxSession);
65
+ return `${prefix}${session}${suffix}`;
66
+ }
67
+
41
68
  /**
42
69
  * 关键词 + 向量融合(A4)的默认权重。
43
70
  *
@@ -49,6 +76,40 @@ export const PRIVACY_HIDDEN_MARK = '[已隐藏]';
49
76
  * semanticAdmit 是「零关键词命中」卡的余弦准入线:本地 bge-small-zh 在无关
50
77
  * 静默查询上的最高余弦实测 0.498,因此 0.55 能挡住沉默样本,又放得进真改写。
51
78
  */
79
+ /** A3 ranking floors: trust 0 => half score; trust 1 => full score; never filters a card out. */
80
+ export const TRUST_RANKING_FLOOR = 0.5;
81
+
82
+ function clamp01(value) {
83
+ if (!Number.isFinite(value)) return 0;
84
+ return Math.max(0, Math.min(1, value));
85
+ }
86
+
87
+ /** Neutral (1.0) unless the card carries a trust hint; no trust hint => legacy ordering. */
88
+ export function trustFactorOf(card, { trustNow = null } = {}) {
89
+ if (!card || typeof card !== 'object') return { factor: 1, trustScore: null, lowTrust: false };
90
+ const hasHint = Number.isFinite(card.trustScore) || typeof card.sourceTier === 'string';
91
+ if (!hasHint) return { factor: 1, trustScore: null, lowTrust: false };
92
+ let score = Number.isFinite(card.trustScore) ? clamp01(card.trustScore) : null;
93
+ if (score === null) {
94
+ // Event-time decay: caller injects the turn/event wall clock; createdAt comes from the card
95
+ // (date/lastUsed). No Date.now() in ranking => deterministic tests and no wall-clock games.
96
+ const created = card.createdAt ?? card.date ?? card.lastUsed ?? null;
97
+ const { trustScore } = trustScoreAt({
98
+ sourceTier: card.sourceTier,
99
+ verifyCount: Number.isInteger(card.verifyCount) ? card.verifyCount : 0,
100
+ createdAt: created,
101
+ now: trustNow ?? null,
102
+ legacy: false,
103
+ });
104
+ score = trustScore;
105
+ }
106
+ return {
107
+ factor: TRUST_RANKING_FLOOR + (1 - TRUST_RANKING_FLOOR) * score,
108
+ trustScore: score,
109
+ lowTrust: score < 0.35,
110
+ };
111
+ }
112
+
52
113
  export const FUSION_DEFAULTS = Object.freeze({
53
114
  keywordWeight: 0.6,
54
115
  vectorWeight: 0.4,
@@ -296,7 +357,7 @@ export function pickExcerpt(card, hits, maxTokens) {
296
357
  }
297
358
 
298
359
  /* ------------------------------------------------------------------ *
299
- * 3. 去重(与已注入的 handover 卡 / engram 事实比字面)
360
+ * 3. 去重(与已注入的 handover 卡 / 外部记忆事实比字面)
300
361
  * ------------------------------------------------------------------ */
301
362
 
302
363
  function bigrams(text) {
@@ -366,7 +427,7 @@ export function maxCosine(queryVector, vectorList) {
366
427
  * localOnly?:boolean}>} input.cards
367
428
  * @param {number} [input.budget] 本次注入的 token 预算
368
429
  * @param {string[]} [input.seenPaths] 本会话已注入过的卡路径(不再重复)
369
- * @param {string[]} [input.injectedTexts] 本会话已注入的其它文本(handover / engram 事实)
430
+ * @param {string[]} [input.injectedTexts] 本会话已注入的其它文本(handover / 外部记忆事实)
370
431
  * @param {object} [input.options] 覆盖 DEFAULTS 的阈值
371
432
  * @param {(text:string)=>{verdict:string}|boolean} [input.options.privacyScan]
372
433
  * 注入前的隐私扫描器;不传则用 defaultPrivacyScan(保守 PII 兜底)。
@@ -398,6 +459,12 @@ export function chooseRecall({
398
459
  && vectors,
399
460
  );
400
461
  const fusion = { ...FUSION_DEFAULTS, ...(opt.fusion || {}) };
462
+ // B2 context channels (default neutral: no inputs => zero bonus, legacy ordering unchanged).
463
+ const queryTimeRefs = Array.isArray(opt.queryTimeRefs) ? opt.queryTimeRefs : [];
464
+ const entityAliases = opt.entityAliases && typeof opt.entityAliases === 'object' && !Array.isArray(opt.entityAliases)
465
+ ? opt.entityAliases
466
+ : null;
467
+ const entityHits = entityAliases ? resolveEntityAliases(queryText, entityAliases).matches : [];
401
468
  const scan = typeof opt.privacyScan === 'function' ? opt.privacyScan : defaultPrivacyScan;
402
469
  const stats = {
403
470
  eligible: 0,
@@ -412,6 +479,8 @@ export function chooseRecall({
412
479
  redactedByPrivacy: 0,
413
480
  semanticCandidates: 0,
414
481
  semanticAdmitted: 0,
482
+ droppedByLowTrust: 0,
483
+ ...(queryTimeRefs.length > 0 || entityAliases ? { contextBoosted: 0 } : {}),
415
484
  };
416
485
 
417
486
  if (typeof queryText !== 'string' || queryText.trim() === '' || !Array.isArray(cards) || cards.length === 0) {
@@ -432,6 +501,7 @@ export function chooseRecall({
432
501
  // 关键词命中仍走原来的 minScore/minHits 门槛;零命中卡只有在余弦
433
502
  // ≥ semanticAdmit 且融合分 ≥ minFusedScore 时才准进来(语义召回的唯一入口)。
434
503
  const eligible = [];
504
+ let trustRanked = 0;
435
505
  for (const c of scored) {
436
506
  const keywordEligible = c.hitCount >= opt.minHits && c.score >= opt.minScore;
437
507
  const semanticEligible = fuse && Number.isFinite(c.cos) && c.cos >= fusion.semanticAdmit;
@@ -450,9 +520,35 @@ export function chooseRecall({
450
520
  stats.semanticCandidates += 1;
451
521
  stats.semanticAdmitted += 1;
452
522
  }
453
- eligible.push({ ...c, fused, reason: fuse ? (c.hitCount > 0 ? 'keyword+vector' : 'vector') : 'keyword' });
523
+ // B2: time affinity (card date in a normalized query time range) + entity alias affinity.
524
+ let contextBonus = 0;
525
+ if (queryTimeRefs.length > 0 && cardTimeAffinity(c.card, queryTimeRefs) > 0) contextBonus += 0.15;
526
+ if (entityHits.length > 0) {
527
+ const cardText = `${c.card?.title ?? ''}\n${c.card?.body ?? ''}`;
528
+ if (entityHits.some((hit) => cardText.includes(hit.canonical) || cardText.includes(hit.alias))) contextBonus += 0.1;
529
+ }
530
+ if (contextBonus > 0 && Object.prototype.hasOwnProperty.call(stats, 'contextBoosted')) stats.contextBoosted += 1;
531
+ const trust = trustFactorOf(c.card, { trustNow: opt.trustNow ?? null });
532
+ if (trust.trustScore !== null) trustRanked += 1;
533
+ // A3 policy: default 'downrank' (data preserved); 'exclude' drops only from injection.
534
+ if (opt.trustPolicy === 'exclude' && trust.lowTrust === true) { stats.droppedByLowTrust += 1; continue; }
535
+ eligible.push({
536
+ ...c,
537
+ fused,
538
+ reason: fuse ? (c.hitCount > 0 ? 'keyword+vector' : 'vector') : 'keyword',
539
+ trustFactor: trust.factor,
540
+ trustScore: trust.trustScore,
541
+ lowTrust: trust.lowTrust,
542
+ contextBonus,
543
+ rankScore: Math.round((fused * trust.factor + contextBonus) * 1000) / 1000,
544
+ });
454
545
  }
455
- eligible.sort((a, b) => b.fused - a.fused || b.score - a.score || b.hitCount - a.hitCount || a.path.localeCompare(b.path));
546
+ // A3: low-trust cards are downranked by score weight, not dropped.
547
+ if (trustRanked > 0) stats.downrankedLowTrust = 0;
548
+ const isEventCard = (cand) => String(cand?.path ?? '').startsWith('auto:');
549
+ eligible.sort((a, b) => (Number(isEventCard(a)) - Number(isEventCard(b)))
550
+ || b.rankScore - a.rankScore || b.fused - a.fused || b.score - a.score || b.hitCount - a.hitCount || a.path.localeCompare(b.path));
551
+ if (trustRanked > 0) stats.downrankedLowTrust = eligible.filter((item) => item.lowTrust === true).length;
456
552
 
457
553
  stats.eligible = eligible.length;
458
554
  stats.best = eligible.length > 0 ? { path: eligible[0].path, score: eligible[0].fused } : null;
@@ -508,6 +604,8 @@ export function chooseRecall({
508
604
  reason: cand.reason,
509
605
  excerpt: PRIVACY_HIDDEN_MARK,
510
606
  privacyRedacted: true,
607
+ ...(cand.cardId ? { cardId: cand.cardId } : {}),
608
+ ...(cand.anchor ? { anchor: cand.anchor } : {}),
511
609
  _hits: [],
512
610
  });
513
611
  remaining -= hiddenCost;
@@ -519,7 +617,7 @@ export function chooseRecall({
519
617
  continue;
520
618
  }
521
619
 
522
- // 去重:与已注入的 handover / engram 事实比字面包含度
620
+ // 去重:与已注入的 handover / 外部记忆事实比字面包含度
523
621
  if (injectedTexts.length > 0) {
524
622
  const dup = injectedTexts.some(
525
623
  (t) => typeof t === 'string' && t.trim() !== '' && containment(excerpt, t) >= opt.dedupeContainment,
@@ -536,6 +634,8 @@ export function chooseRecall({
536
634
  score: cand.fused,
537
635
  reason: cand.reason,
538
636
  excerpt,
637
+ ...(cand.cardId ? { cardId: cand.cardId } : {}),
638
+ ...(cand.anchor ? { anchor: cand.anchor } : {}),
539
639
  _hits: cand.hits.map((h) => h.phrase),
540
640
  });
541
641
  remaining -= cost;
@@ -566,10 +666,10 @@ export function renderItemHeader(item, { showScore = true, withPath = true, priv
566
666
  const meta = [];
567
667
  if (withPath) {
568
668
  const scan = typeof privacyScan === 'function' ? privacyScan : defaultPrivacyScan;
569
- meta.push(privacyBlocked(scan, item.path) ? PRIVACY_HIDDEN_PATH : item.path);
669
+ meta.push(privacyBlocked(scan, item.path) ? PRIVACY_HIDDEN_PATH : sanitizeDataText(item.path, 200));
570
670
  }
571
671
  if (showScore) meta.push(`相关度 ${Number(item.score).toFixed(2)}`);
572
- return `- 《${item.title}》(${meta.join(' · ')})`;
672
+ return `- 《${sanitizeDataText(item.title, 160)}》(${meta.join(' · ')})`;
573
673
  }
574
674
 
575
675
  /* ------------------------------------------------------------------ *
@@ -587,8 +687,14 @@ export function renderInjection(result, opts = {}) {
587
687
  const lines = [header];
588
688
  for (const item of items) {
589
689
  lines.push(renderItemHeader(item, opts));
590
- const body = String(item.excerpt || '').split('\n').filter((l) => l.trim() !== '');
591
- for (const line of body) lines.push(` ${line}`);
690
+ // Card body is data: one inert line, never multiple model-readable lines.
691
+ const body = sanitizeDataText(item.excerpt || '', 400);
692
+ if (body !== '') lines.push(` ${body}`);
693
+ // 1.8.0 AutoCard: origin line is generated only from the validated anchor.
694
+ if (item && item.anchor) {
695
+ const origin = renderAnchorDataLine(item.anchor);
696
+ if (origin !== '') lines.push(` ${origin}`);
697
+ }
592
698
  }
593
699
  return lines.join('\n');
594
700
  }
@@ -0,0 +1,302 @@
1
+ // src/recall/shared-visibility.js — M2-C R1–R6:双我可见的共享可见块
2
+ //
3
+ // 新会话首轮 agent/pre-step 注入一块 ≤300 字的「共享可见块」(session-reference
4
+ // 隐藏消息),让当前会话看到**其它活体会话**的目标 / 末活动 / 最后真实用户消息 /
5
+ // 最后记忆写入 / 陈旧标记,无需读原始日志(R1:≤2s;R3:不拿心跳冒充实时)。
6
+ //
7
+ // 数据源:
8
+ // - 活体表:liveList(与面板 #23 frames 同口径,src/live.js)
9
+ // - 记忆写入:resolveMemoryWriteAt(与 memory-gap 同源,src/lifecycle/gap-clocks.js)
10
+ // - 最后真实用户消息:事件索引(.longlive/index/events.db,kind=user),
11
+ // 无索引时退回 live 行扩展字段;两者都没有 => 不猜(lastUserMessage=null)
12
+ //
13
+ // 纪律:只读;失败静默(不阻断 turn);隐私门命中整行丢弃;≤300 字;只首轮注入。
14
+
15
+ import { liveList } from '../live.js';
16
+ import { resolveMemoryWriteAt } from '../lifecycle/gap-clocks.js';
17
+
18
+ export const SHARED_VISIBILITY_SCHEMA = 'shared-visibility/v1';
19
+ export const SHARED_BLOCK_MAX_CHARS = 300;
20
+ export const GOAL_STALE_MS = 5 * 60 * 1000;
21
+ export const SESSION_STALE_MS = 30 * 60 * 1000;
22
+ export const ACK_WORDS = Object.freeze(['对', '好', '嗯', '收到', '可以', '是的', '行', '不错', '好的', '知道了', 'ok', 'OK']);
23
+ const ACK_RE = new RegExp(`^(?:${ACK_WORDS.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})[。.!!??~~ ]*$`, 'u');
24
+
25
+ function toMs(value) {
26
+ if (value instanceof Date) return value.getTime();
27
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
28
+ if (typeof value === 'string' && value.trim() !== '') {
29
+ const parsed = Date.parse(value);
30
+ return Number.isFinite(parsed) ? parsed : null;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function clampText(value, max) {
36
+ const text = String(value ?? '').replace(/\s+/gu, ' ').trim();
37
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
38
+ }
39
+
40
+ /** 「无信息量」判定:长度 <6 或命中 ack 词表(「对的」不是最后意图)。 */
41
+ export function isInformativeUserText(text) {
42
+ const value = String(text ?? '').trim();
43
+ if (value === '') return false;
44
+ if ([...value].length < 6) return false;
45
+ return !ACK_RE.test(value);
46
+ }
47
+
48
+ function isoWithOffset(ms, offsetMinutes = 480) {
49
+ if (!Number.isFinite(ms)) return null;
50
+ const shifted = new Date(ms + offsetMinutes * 60 * 1000);
51
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
52
+ return `${shifted.getUTCFullYear()}-${pad(shifted.getUTCMonth() + 1)}-${pad(shifted.getUTCDate())}T${pad(shifted.getUTCHours())}:${pad(shifted.getUTCMinutes())}:${pad(shifted.getUTCSeconds())}.${pad(shifted.getUTCMilliseconds(), 3)}+08:00`;
53
+ }
54
+
55
+ function clockText(ms, offsetMinutes = 480) {
56
+ if (!Number.isFinite(ms)) return '';
57
+ const shifted = new Date(ms + offsetMinutes * 60 * 1000);
58
+ const pad = (n) => String(n).padStart(2, '0');
59
+ return `${pad(shifted.getUTCHours())}:${pad(shifted.getUTCMinutes())}:${pad(shifted.getUTCSeconds())}`;
60
+ }
61
+
62
+ function ageText(ms) {
63
+ if (!Number.isFinite(ms) || ms < 0) return '';
64
+ const minutes = Math.floor(ms / 60_000);
65
+ if (minutes < 1) return '刚刚';
66
+ if (minutes < 60) return `${minutes} 分钟前`;
67
+ const hours = Math.floor(minutes / 60);
68
+ if (hours < 24) return `${hours} 小时前`;
69
+ return `${Math.floor(hours / 24)} 天前`;
70
+ }
71
+
72
+ function pickLastUserMessage(sessionId, lastUserMessages) {
73
+ const entry = lastUserMessages?.[sessionId] ?? null;
74
+ if (!entry || typeof entry !== 'object') return null;
75
+ if (typeof entry.text !== 'string' || entry.text.trim() === '') return null;
76
+ const text = clampText(entry.text, 80);
77
+ const prev = Array.isArray(entry.prev)
78
+ ? entry.prev.filter((item) => item && typeof item.text === 'string' && item.text.trim() !== '').slice(0, 2).map((item) => ({ text: clampText(item.text, 60), at: item.at ?? null }))
79
+ : [];
80
+ return { text, at: entry.at ?? null, informative: isInformativeUserText(text), prev };
81
+ }
82
+
83
+ /**
84
+ * Pure core: build the shared-visibility rows from live entries + injected data.
85
+ * @returns {{schema,sessions:object[],injectedAt:string,count:number}|null}
86
+ */
87
+ export function buildSharedVisibility({
88
+ entries = [],
89
+ currentSessionId = null,
90
+ now = Date.now(),
91
+ memoryWrite = null,
92
+ lastUserMessages = {},
93
+ projectOf = null,
94
+ maxSessions = 2,
95
+ } = {}) {
96
+ const nowMs = toMs(now) ?? Date.now();
97
+ const sessions = [];
98
+ for (const entry of Array.isArray(entries) ? entries : []) {
99
+ if (!entry || typeof entry !== 'object') continue;
100
+ if (entry.sessionId === currentSessionId) continue;
101
+ if (entry.effectiveState === 'dead' || entry.effectiveState === 'corrupt') continue;
102
+ const updatedMs = toMs(entry.updatedAt);
103
+ const goalAtMs = toMs(entry.goalUpdatedAt) ?? updatedMs;
104
+ const goalText = clampText(entry.goal, 80);
105
+ const action = entry.last_action && typeof entry.last_action === 'object' ? entry.last_action : null;
106
+ const actionAtMs = toMs(action?.at);
107
+ const attribution = {
108
+ present: entry.sessionId != null,
109
+ // 归属校验 = 行里同时有 host+pid 证据;不拿心跳冒充实时(R3)。
110
+ verified: typeof entry.host === 'string' && entry.host.trim() !== '' && Number.isInteger(entry.pid),
111
+ };
112
+ sessions.push({
113
+ project: typeof projectOf === 'function' ? (projectOf(entry) ?? null) : (entry.project ?? null),
114
+ sessionId: entry.sessionId,
115
+ attribution,
116
+ goal: goalText === '' ? null : {
117
+ text: goalText,
118
+ ageMs: Number.isFinite(goalAtMs) ? Math.max(0, nowMs - goalAtMs) : null,
119
+ stale: !Number.isFinite(goalAtMs) || nowMs - goalAtMs > GOAL_STALE_MS,
120
+ },
121
+ lastAction: action === null ? null : {
122
+ kind: clampText(action.kind ?? action.type ?? 'action', 24),
123
+ detail: clampText(action.detail ?? action.summary ?? '', 120),
124
+ at: isoWithOffset(actionAtMs),
125
+ ageMs: Number.isFinite(actionAtMs) ? Math.max(0, nowMs - actionAtMs) : null,
126
+ stale: !Number.isFinite(actionAtMs) || nowMs - actionAtMs > GOAL_STALE_MS,
127
+ },
128
+ lastUserMessage: pickLastUserMessage(entry.sessionId, lastUserMessages),
129
+ lastMemoryWrite: memoryWrite && Number.isFinite(toMs(memoryWrite.atMs))
130
+ ? {
131
+ at: isoWithOffset(toMs(memoryWrite.atMs)),
132
+ sourceFile: memoryWrite.rel ?? memoryWrite.sourceFile ?? null,
133
+ gapSec: Number.isFinite(memoryWrite.gapSec) ? memoryWrite.gapSec : (Number.isFinite(toMs(memoryWrite.atMs)) ? Math.max(0, Math.floor((nowMs - toMs(memoryWrite.atMs)) / 1000)) : null),
134
+ }
135
+ : null,
136
+ injectedAt: isoWithOffset(nowMs),
137
+ stale: !Number.isFinite(updatedMs) || nowMs - updatedMs > SESSION_STALE_MS,
138
+ effectiveState: entry.effectiveState ?? entry.state ?? null,
139
+ });
140
+ if (sessions.length >= Math.max(1, Number(maxSessions) || 2)) break;
141
+ }
142
+ if (sessions.length === 0) return null;
143
+ return { schema: SHARED_VISIBILITY_SCHEMA, sessions, injectedAt: isoWithOffset(nowMs), count: sessions.length };
144
+ }
145
+
146
+ /** Render the ≤300-char hidden block. Privacy hits drop the whole line (never the turn). */
147
+ export function renderSharedVisibilityBlock(visibility, { maxChars = SHARED_BLOCK_MAX_CHARS, privacyScan = null } = {}) {
148
+ if (!visibility || !Array.isArray(visibility.sessions) || visibility.sessions.length === 0) return '';
149
+ const scan = typeof privacyScan === 'function' ? privacyScan : null;
150
+ const safe = (text) => {
151
+ if (typeof text !== 'string' || text === '') return '';
152
+ if (scan !== null) {
153
+ try {
154
+ if (scan(text)?.verdict === 'local_only') return '';
155
+ } catch { return ''; }
156
+ }
157
+ return text;
158
+ };
159
+ const lines = [`【共享可见 · 只读】现在 ${visibility.count} 个思维活着。`];
160
+ let memoryWriteLine = '';
161
+ for (const session of visibility.sessions) {
162
+ const shortId = session.sessionId ? session.sessionId.slice(0, 18) : 'unknown';
163
+ const project = session.project ? `(${session.project})` : '';
164
+ const goal = session.goal
165
+ ? `目标:${safe(session.goal.text)}${session.goal.stale ? `(${ageText(session.goal.ageMs)}·陈旧)` : ''}`
166
+ : '';
167
+ const action = session.lastAction
168
+ ? `末活动:${safe(session.lastAction.kind)}/${safe(session.lastAction.detail)}${Number.isFinite(session.lastAction.ageMs) ? `(${ageText(session.lastAction.ageMs)})` : ''}`
169
+ : '';
170
+ const user = session.lastUserMessage
171
+ ? `最后用户消息:「${safe(session.lastUserMessage.text)}」${session.lastUserMessage.informative ? '' : '(无信息量)'}${session.lastUserMessage.prev.length > 0 ? `;前一条:「${safe(session.lastUserMessage.prev[0].text)}」` : ''}`
172
+ : '';
173
+ const row = [shortId + project, goal, action, user].filter(Boolean).join('|');
174
+ if (row !== '') lines.push(`· ${row}`);
175
+ if (session.lastMemoryWrite && session.lastMemoryWrite.sourceFile) {
176
+ memoryWriteLine = `· 最后记忆写入 ${clockText(toMs(session.lastMemoryWrite.at))} · 来源 ${safe(String(session.lastMemoryWrite.sourceFile))}`;
177
+ }
178
+ if (session.stale && session.lastAction === null) lines.push('· (该会话已 >30 分钟无心跳,按陈旧处理)');
179
+ }
180
+ if (memoryWriteLine !== '') lines.push(memoryWriteLine);
181
+ let text = lines.join('\n');
182
+ if (text.length > maxChars && memoryWriteLine !== '') {
183
+ // R1: the memory-write line is contract data (golden) — keep it even when
184
+ // trimming the rest of the block to the ≤300-char budget.
185
+ const header = lines[0];
186
+ const bodyLines = lines.slice(1).filter((line) => line !== memoryWriteLine);
187
+ let budget = maxChars - header.length - memoryWriteLine.length - 2;
188
+ if (budget < 0) {
189
+ text = `${header}\n${memoryWriteLine}`.slice(0, Math.max(0, maxChars - 1)) + '…';
190
+ return text;
191
+ }
192
+ const kept = [];
193
+ for (const line of bodyLines) {
194
+ if (line.length <= budget) {
195
+ kept.push(line);
196
+ budget -= line.length + 1;
197
+ } else {
198
+ break;
199
+ }
200
+ }
201
+ text = [header, ...kept, memoryWriteLine].join('\n');
202
+ }
203
+ if (text.length > maxChars) text = `${text.slice(0, Math.max(0, maxChars - 1))}…`;
204
+ return text;
205
+ }
206
+
207
+ function readIndexLastUserMessages({ memoryRoot, sessionIds = [] } = {}) {
208
+ const wanted = new Set(Array.isArray(sessionIds) ? sessionIds : []);
209
+ if (wanted.size === 0) return {};
210
+ try {
211
+ // Lazy import keeps the module cheap when no index exists.
212
+ const eventIndex = requireEventIndex();
213
+ const dbPath = `${String(memoryRoot)}/.longlive/index/events.db`;
214
+ if (!eventIndex || !dbPath) return {};
215
+ const fs = requireFs();
216
+ const path = requirePath();
217
+ const resolved = path.resolve(String(memoryRoot));
218
+ if (!fs.existsSync(path.join(resolved, '.longlive', 'index', 'events.db'))) return {};
219
+ const index = eventIndex.openEventIndex({ memoryRoot: resolved });
220
+ try {
221
+ const rows = index.listEvents({ kind: 'user', limit: 200 });
222
+ const out = {};
223
+ for (const sessionId of wanted) {
224
+ const own = rows.filter((row) => row.session_id === sessionId)
225
+ .sort((a, b) => (toMs(b.time_end) ?? 0) - (toMs(a.time_end) ?? 0));
226
+ if (own.length === 0) continue;
227
+ const text = String(own[0].summary ?? '').trim();
228
+ if (text === '') continue;
229
+ out[sessionId] = {
230
+ text,
231
+ at: isoWithOffset(toMs(own[0].time_end)),
232
+ prev: own.slice(1, 3).map((row) => ({ text: String(row.summary ?? '').trim(), at: isoWithOffset(toMs(row.time_end)) })),
233
+ };
234
+ }
235
+ return out;
236
+ } finally {
237
+ index.close();
238
+ }
239
+ } catch {
240
+ return {};
241
+ }
242
+ }
243
+
244
+ // Node builtins are loaded lazily so the pure builders stay dependency-free at import time.
245
+ let cachedEventIndex = null;
246
+ let cachedFs = null;
247
+ let cachedPath = null;
248
+ function requireEventIndex() {
249
+ return cachedEventIndex;
250
+ }
251
+ function requireFs() {
252
+ return cachedFs;
253
+ }
254
+ function requirePath() {
255
+ return cachedPath;
256
+ }
257
+ async function loadDeps() {
258
+ try {
259
+ cachedEventIndex = await import('../memory/event-index.js');
260
+ cachedFs = await import('node:fs');
261
+ cachedPath = await import('node:path');
262
+ } catch {
263
+ cachedEventIndex = null;
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Production wrapper (read-only). Missing pieces degrade to null, never throw.
269
+ * @returns {null|{schema,sessions,injectedAt,count,block}}
270
+ */
271
+ export function sharedVisibility({ memoryRoot, currentSessionId = null, now = Date.now(), privacyScan = null, projectOf = null } = {}) {
272
+ try {
273
+ loadDepsSync();
274
+ const nowMs = toMs(now) ?? Date.now();
275
+ const listed = liveList({ includeDead: false }, { memoryRoot, now: nowMs });
276
+ const entries = Array.isArray(listed?.entries) ? listed.entries : [];
277
+ if (entries.length === 0) return null;
278
+ let memoryWrite = null;
279
+ try {
280
+ const resolved = resolveMemoryWriteAt({ memoryRoot, bootAtMs: nowMs });
281
+ if (resolved && Number.isFinite(resolved.atMs)) memoryWrite = { atMs: resolved.atMs, rel: resolved.rel };
282
+ } catch { /* memory write is optional */ }
283
+ const sessionIds = entries.map((entry) => entry.sessionId).filter((id) => id != null && id !== currentSessionId);
284
+ const lastUserMessages = readIndexLastUserMessages({ memoryRoot, sessionIds });
285
+ const visibility = buildSharedVisibility({ entries, currentSessionId, now: nowMs, memoryWrite, lastUserMessages, projectOf });
286
+ if (visibility === null) return null;
287
+ const block = renderSharedVisibilityBlock(visibility, { privacyScan });
288
+ return block === '' ? null : { ...visibility, block };
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
293
+
294
+ // Resolve lazy deps synchronously via createRequire (fs/path) and a static import of the index.
295
+ import fsDefault from 'node:fs';
296
+ import pathDefault from 'node:path';
297
+ import * as eventIndexModule from '../memory/event-index.js';
298
+ function loadDepsSync() {
299
+ cachedEventIndex = eventIndexModule;
300
+ cachedFs = fsDefault;
301
+ cachedPath = pathDefault;
302
+ }