@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
@@ -0,0 +1,401 @@
1
+ // src/memory/cards.js — 1.8.0 AutoCard: EventRecord → append-only index card.
2
+ //
3
+ // Minimal deterministic distillation: no LLM, no network, no mutation of the
4
+ // event index. One card per turn EventRecord, keyed by eventId; repeated
5
+ // captureTurn calls never append a second line for the same eventId.
6
+ //
7
+ // Card shape (spec):
8
+ // { schema, cardId, eventId, sessionId, turn, at, kind, title, terms,
9
+ // anchor: { sessionId, turn, seqStart, seqEnd, digest } }
10
+ //
11
+ // anchor is a copied projection of EventRecord.anchors[0] (the session_turn
12
+ // anchor), so a card can always be verified back against the original turn.
13
+
14
+ import crypto from 'node:crypto';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+
18
+ import { openEventIndex } from './event-index.js';
19
+ import { acquireLease, releaseLease } from './lock.js';
20
+ import { resolveHostKey, readHostKey } from '../role/writeprotect.js';
21
+
22
+ export const AUTO_CARD_SCHEMA = 'auto-card/v1';
23
+ export const CARDS_REL = path.join('.longlive', 'index', 'cards.jsonl');
24
+ export const CARD_TITLE_MAX = 80;
25
+ export const CARD_ANCHOR_HINT_MAX = 60;
26
+
27
+ let cache = null;
28
+
29
+ export function cardsPathOf(memoryRoot) {
30
+ return path.join(path.resolve(String(memoryRoot)), CARDS_REL);
31
+ }
32
+
33
+ function sha256Hex(text) {
34
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
35
+ }
36
+
37
+ function macHex(key, text) {
38
+ return crypto.createHmac('sha256', key).update(String(text), 'utf8').digest('hex');
39
+ }
40
+
41
+ /** Canonical signed payload: every trusted card field except the signature. */
42
+ export function autoCardPayload(card) {
43
+ return {
44
+ schema: card?.schema ?? AUTO_CARD_SCHEMA,
45
+ cardId: card?.cardId ?? '',
46
+ eventId: card?.eventId ?? '',
47
+ sourceEventId: card?.sourceEventId ?? card?.eventId ?? '',
48
+ sessionId: card?.sessionId ?? '',
49
+ turn: card?.turn ?? null,
50
+ at: card?.at ?? '',
51
+ kind: card?.kind ?? '',
52
+ title: card?.title ?? '',
53
+ terms: Array.isArray(card?.terms) ? card.terms : [],
54
+ anchor: card?.anchor ?? null,
55
+ salience: card?.salience ?? 1,
56
+ retention: card?.retention ?? null,
57
+ };
58
+ }
59
+
60
+ export function signAutoCard(card, auditKey) {
61
+ if (!auditKey) return { ...card, sig: null };
62
+ return { ...card, sig: macHex(auditKey, JSON.stringify(autoCardPayload(card))) };
63
+ }
64
+
65
+ function oneLine(text, max = 160) {
66
+ return String(text ?? '').replace(/\s+/gu, ' ').trim().slice(0, max);
67
+ }
68
+
69
+ export function cardIdOf(eventId) {
70
+ return `card:${sha256Hex(String(eventId ?? '')).slice(0, 16)}`;
71
+ }
72
+
73
+ /** Full-file SHA-256: any byte change invalidates the cache, even mid-file. */
74
+ export function sampleContentHash(file, size = null) {
75
+ try {
76
+ return sha256Hex(fs.readFileSync(file));
77
+ } catch {
78
+ return '';
79
+ }
80
+ }
81
+
82
+ function anchorOf(record) {
83
+ const anchor = Array.isArray(record?.anchors) && record.anchors.length > 0 ? record.anchors[0] : null;
84
+ if (!anchor || typeof anchor !== 'object') return null;
85
+ if (typeof anchor.sessionId !== 'string' || anchor.sessionId === '') return null;
86
+ if (!Number.isInteger(anchor.turn) || !Number.isInteger(anchor.seqStart) || !Number.isInteger(anchor.seqEnd)) return null;
87
+ if (typeof anchor.digest !== 'string' || !/^[0-9a-f]{64}$/.test(anchor.digest)) return null;
88
+ // Copy only stable anchor fields (no status mutation is written here).
89
+ return {
90
+ sessionId: anchor.sessionId,
91
+ turn: anchor.turn,
92
+ seqStart: anchor.seqStart,
93
+ seqEnd: anchor.seqEnd,
94
+ digest: anchor.digest,
95
+ };
96
+ }
97
+
98
+ /** Pure EventRecord → AutoCard (or null when the record is not anchorable). */
99
+ export function makeAutoCard(record, { now = () => new Date(), auditKey = null } = {}) {
100
+ if (!record || typeof record !== 'object') return null;
101
+ const eventId = typeof record.id === 'string' ? record.id : '';
102
+ const anchor = anchorOf(record);
103
+ if (eventId === '' || anchor === null) return null;
104
+ // Turn 0 is the session preamble (system/tool scaffolding), not a user turn.
105
+ if (!Number.isInteger(record.turn) || record.turn < 1) return null;
106
+ const atMs = Number.isFinite(record.timeEnd) ? record.timeEnd
107
+ : (Number.isFinite(record.timeStart) ? record.timeStart : now().getTime());
108
+ const title = oneLine(record.summary, CARD_TITLE_MAX);
109
+ const terms = Array.isArray(record.terms)
110
+ ? [...new Set(record.terms.map((term) => oneLine(term, 40)).filter((term) => term !== ''))].slice(0, 20)
111
+ : [];
112
+ return {
113
+ schema: AUTO_CARD_SCHEMA,
114
+ cardId: cardIdOf(eventId),
115
+ eventId,
116
+ sourceEventId: eventId,
117
+ sessionId: String(record.sessionId ?? anchor.sessionId),
118
+ turn: Number.isInteger(record.turn) ? record.turn : anchor.turn,
119
+ at: new Date(atMs).toISOString(),
120
+ kind: oneLine(record.kind, 40) || 'chat',
121
+ title,
122
+ terms,
123
+ anchor,
124
+ // Event cards decay fast but are never physically deleted (append-only,
125
+ // no forget execution); salience is intentionally lower than curated cards.
126
+ salience: 1,
127
+ retention: { decay: 'fast', ttlDays: 30 },
128
+ ...(auditKey ? { sig: macHex(auditKey, JSON.stringify(autoCardPayload({
129
+ schema: AUTO_CARD_SCHEMA,
130
+ cardId: cardIdOf(eventId),
131
+ eventId,
132
+ sourceEventId: eventId,
133
+ sessionId: String(record.sessionId ?? anchor.sessionId),
134
+ turn: Number.isInteger(record.turn) ? record.turn : anchor.turn,
135
+ at: new Date(atMs).toISOString(),
136
+ kind: oneLine(record.kind, 40) || 'chat',
137
+ title,
138
+ terms,
139
+ anchor,
140
+ salience: 1,
141
+ retention: { decay: 'fast', ttlDays: 30 },
142
+ }))) } : {}),
143
+ };
144
+ }
145
+
146
+ export function readAutoCards(memoryRoot) {
147
+ const file = cardsPathOf(memoryRoot);
148
+ let stat = null;
149
+ try { stat = fs.statSync(file); } catch { cache = { file: null, mtimeMs: 0, size: 0, contentHash: '', cards: [] }; return cache.cards; }
150
+ const contentHash = sampleContentHash(file, stat.size);
151
+ if (cache && cache.file === file && cache.mtimeMs === stat.mtimeMs && cache.size === stat.size
152
+ && cache.contentHash === contentHash) return cache.cards;
153
+ const out = [];
154
+ let raw = '';
155
+ try { raw = fs.readFileSync(file, 'utf8'); } catch { return out; }
156
+ for (const line of raw.split('\n')) {
157
+ if (line.trim() === '') continue;
158
+ try {
159
+ const card = JSON.parse(line);
160
+ if (!card || typeof card.cardId !== 'string' || typeof card.eventId !== 'string') continue;
161
+ // Reject injection-shaped raw rows early (the trusted read path also
162
+ // verifies the HMAC + EventRecord; this keeps inspection APIs inert).
163
+ const unsafe = (value) => typeof value === 'string' && /[\u0000-\u001f\u007f-\u009f\u202a-\u202e]/u.test(value);
164
+ const unsafeAnchor = card.anchor && typeof card.anchor.sessionId === 'string' && unsafe(card.anchor.sessionId);
165
+ const unsafeTerms = Array.isArray(card.terms) && card.terms.some((term) => unsafe(term));
166
+ if (unsafe(card.title) || unsafeTerms || unsafeAnchor) continue;
167
+ out.push(card);
168
+ } catch { /* corrupt line is skipped; append-only file is never rewritten */ }
169
+ }
170
+ cache = { file, mtimeMs: stat.mtimeMs, size: stat.size, contentHash, cards: out };
171
+ return out;
172
+ }
173
+
174
+ /** True when the card's anchor is exactly the EventRecord's primary anchor. */
175
+ export function validateAutoCardRecord(card, event, auditKey = null) {
176
+ if (!card || !event) return { ok: false, code: 'E_CARD_EVENT_MISSING' };
177
+ if (!auditKey || typeof card.sig !== 'string' || card.sig === '') return { ok: false, code: 'E_CARD_UNSIGNED' };
178
+ const expectedSig = macHex(auditKey, JSON.stringify(autoCardPayload(card)));
179
+ if (card.sig !== expectedSig) return { ok: false, code: 'E_CARD_SIG_INVALID' };
180
+ if (typeof card.eventId !== 'string' || card.eventId !== event.id) return { ok: false, code: 'E_CARD_EVENT_MISMATCH' };
181
+ if (card.sourceEventId !== event.id) return { ok: false, code: 'E_CARD_SOURCE_MISMATCH' };
182
+ if (card.cardId !== cardIdOf(card.eventId)) return { ok: false, code: 'E_CARD_ID_MISMATCH' };
183
+ const anchor = card.anchor;
184
+ const expected = Array.isArray(event.anchors) ? event.anchors[0] : null;
185
+ if (!anchor || !expected) return { ok: false, code: 'E_CARD_ANCHOR_MISSING' };
186
+ for (const key of ['sessionId', 'turn', 'seqStart', 'seqEnd', 'digest']) {
187
+ if (anchor[key] !== expected[key]) return { ok: false, code: 'E_CARD_ANCHOR_MISMATCH' };
188
+ }
189
+ if (anchor.digest !== event.digest) return { ok: false, code: 'E_CARD_DIGEST_MISMATCH' };
190
+ return { ok: true, code: 'OK' };
191
+ }
192
+
193
+ /** Trusted content is re-derived from the EventRecord, never from the card file. */
194
+ export function trustedCardFromEvent(card, event) {
195
+ const anchor = Array.isArray(event?.anchors) ? event.anchors[0] : null;
196
+ const atMs = Number.isFinite(event?.timeEnd) ? event.timeEnd
197
+ : (Number.isFinite(event?.timeStart) ? event.timeStart : Date.parse(card?.at ?? '') || 0);
198
+ return {
199
+ schema: AUTO_CARD_SCHEMA,
200
+ cardId: cardIdOf(event.id),
201
+ eventId: event.id,
202
+ sourceEventId: event.id,
203
+ sessionId: String(event.sessionId ?? anchor?.sessionId ?? ''),
204
+ turn: Number.isInteger(event.turn) ? event.turn : anchor?.turn,
205
+ at: new Date(atMs).toISOString(),
206
+ kind: oneLine(event.kind, 40) || 'chat',
207
+ title: oneLine(event.summary, CARD_TITLE_MAX),
208
+ terms: Array.isArray(event.terms) ? event.terms.slice(0, 20) : [],
209
+ anchor: anchor ? {
210
+ sessionId: anchor.sessionId,
211
+ turn: anchor.turn,
212
+ seqStart: anchor.seqStart,
213
+ seqEnd: anchor.seqEnd,
214
+ digest: anchor.digest,
215
+ } : null,
216
+ salience: 1,
217
+ retention: { decay: 'fast', ttlDays: 30 },
218
+ sig: card?.sig ?? null,
219
+ };
220
+ }
221
+
222
+ function auditInvalidCard(memoryRoot, card, result) {
223
+ try {
224
+ const file = path.join(path.resolve(String(memoryRoot)), '.longlive', 'index', 'card-audit.jsonl');
225
+ fs.mkdirSync(path.dirname(file), { recursive: true });
226
+ fs.appendFileSync(file, `${JSON.stringify({
227
+ schema: 'card-audit/v1',
228
+ at: new Date().toISOString(),
229
+ action: 'drop',
230
+ cardId: card?.cardId ?? null,
231
+ eventId: card?.eventId ?? null,
232
+ code: result?.code ?? 'E_CARD_INVALID',
233
+ })}\n`, 'utf8');
234
+ } catch { /* audit failure must not turn into a recall hit */ }
235
+ }
236
+
237
+ let verifiedCache = null;
238
+
239
+ /**
240
+ * Production reader: only cards whose source EventRecord exists in events.db
241
+ * and whose anchor/digest match that record are returned. Invalid cards are
242
+ * dropped and audited; they never reach recall/injection.
243
+ */
244
+ export function readVerifiedAutoCards({ memoryRoot } = {}) {
245
+ const root = path.resolve(String(memoryRoot ?? ''));
246
+ const file = cardsPathOf(root);
247
+ let stat;
248
+ try { stat = fs.statSync(file); } catch { return []; }
249
+ const dbPath = path.join(root, '.longlive', 'index', 'events.db');
250
+ let dbStat;
251
+ try { dbStat = fs.statSync(dbPath); } catch { return []; } // no source of truth => fail closed
252
+ const contentHash = sampleContentHash(file, stat.size);
253
+ const auditKey = readHostKey(root);
254
+ const key = `${file}|${stat.mtimeMs}|${stat.size}|${contentHash}|${dbStat.mtimeMs}|${dbStat.size}|${auditKey ? sha256Hex(auditKey).slice(0, 16) : 'nokey'}`;
255
+ if (verifiedCache && verifiedCache.key === key) return verifiedCache.cards;
256
+ const cards = readAutoCards(root);
257
+ if (!auditKey) {
258
+ for (const card of cards) auditInvalidCard(root, card, { code: 'E_NO_KEY' });
259
+ verifiedCache = { key, cards: [] };
260
+ return [];
261
+ }
262
+ const index = openEventIndex({ memoryRoot: root });
263
+ const verified = [];
264
+ const seen = new Map();
265
+ try {
266
+ for (const card of cards) {
267
+ if (seen.has(card.eventId)) {
268
+ // The first valid signed card wins; a later forged duplicate can never
269
+ // suppress the genuine card (write order alone is not authority).
270
+ auditInvalidCard(root, card, { code: 'E_CARD_DUPLICATE' });
271
+ continue;
272
+ }
273
+ const event = index.getEvent(card.eventId);
274
+ const result = validateAutoCardRecord(card, event, auditKey);
275
+ if (result.ok) {
276
+ const trusted = trustedCardFromEvent(card, event);
277
+ verified.push(trusted);
278
+ seen.set(card.eventId, trusted);
279
+ } else auditInvalidCard(root, card, result);
280
+ }
281
+ } finally {
282
+ index.close();
283
+ }
284
+ verifiedCache = { key, cards: verified };
285
+ return verified;
286
+ }
287
+
288
+ /**
289
+ * Append cards for records, idempotent by eventId.
290
+ * `dryRun` performs the same read/dedupe pass without writing.
291
+ */
292
+ export function appendAutoCards({ memoryRoot, records, now = () => new Date(), dryRun = false, skipLock = false, resign = false } = {}) {
293
+ const root = path.resolve(String(memoryRoot ?? ''));
294
+ if (root === '') throw new Error('E_MEMORY_ROOT: appendAutoCards requires memoryRoot');
295
+ const run = () => {
296
+ const file = cardsPathOf(root);
297
+ const existingCards = readAutoCards(root);
298
+ const existing = new Map(existingCards.map((card) => [card.eventId, card]));
299
+ const existingByCardId = new Map(existingCards.map((card) => [card.cardId, card]));
300
+ let auditKey = null;
301
+ try { auditKey = resolveHostKey(root); } catch { auditKey = null; }
302
+ const cards = [];
303
+ let skipped = 0;
304
+ for (const record of Array.isArray(records) ? records : []) {
305
+ const card = makeAutoCard(record, { now, auditKey });
306
+ if (card === null) { skipped += 1; continue; }
307
+ const sameCardId = existingByCardId.get(card.cardId);
308
+ if (sameCardId && sameCardId.eventId !== card.eventId) {
309
+ auditInvalidCard(root, card, { code: 'E_CARD_ID_COLLISION' });
310
+ skipped += 1;
311
+ continue;
312
+ }
313
+ const prior = existing.get(card.eventId);
314
+ if (prior && !resign) {
315
+ // A signed card may replace an old unsigned draft (migration redo);
316
+ // otherwise eventId is already final.
317
+ if (prior.sig && card.sig) { skipped += 1; continue; }
318
+ if (prior.sig && !card.sig) { skipped += 1; continue; }
319
+ if (!card.sig) { skipped += 1; continue; }
320
+ }
321
+ if (resign && !card.sig) { skipped += 1; continue; }
322
+ existing.set(card.eventId, card);
323
+ existingByCardId.set(card.cardId, card);
324
+ cards.push(card);
325
+ }
326
+ if (!dryRun && cards.length > 0) {
327
+ fs.mkdirSync(path.dirname(file), { recursive: true });
328
+ fs.appendFileSync(file, cards.map((card) => JSON.stringify(card)).join('\n') + '\n', 'utf8');
329
+ }
330
+ return { ok: true, file, written: cards.length, skipped, cards, dryRun };
331
+ };
332
+ if (skipLock || dryRun) return run();
333
+ const lockFile = path.join(root, '.longlive', 'memory', 'cards.lock');
334
+ fs.mkdirSync(path.dirname(lockFile), { recursive: true });
335
+ const holder = `cards-${process.pid}-${Date.now()}`;
336
+ const deadline = Date.now() + 3000;
337
+ let lease = null;
338
+ for (;;) {
339
+ try {
340
+ lease = acquireLease(lockFile, { holder });
341
+ break;
342
+ } catch (error) {
343
+ if (error?.code !== 'E_LOCKED' || Date.now() >= deadline) throw error;
344
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
345
+ }
346
+ }
347
+ try {
348
+ return run();
349
+ } finally {
350
+ releaseLease(lockFile, holder);
351
+ }
352
+ }
353
+
354
+ /** One-line anchor hint for injection, capped at 60 chars. */
355
+ export function anchorHintOf(card) {
356
+ const anchor = card?.anchor;
357
+ if (!anchor || typeof anchor.sessionId !== 'string' || !Number.isInteger(anchor.turn)) return '';
358
+ const suffix = `#${anchor.turn}]`;
359
+ const prefix = '[原文 ';
360
+ const maxSession = Math.max(1, CARD_ANCHOR_HINT_MAX - prefix.length - suffix.length);
361
+ const session = anchor.sessionId.length > maxSession ? anchor.sessionId.slice(0, maxSession) : anchor.sessionId;
362
+ return `${prefix}${session}${suffix}`;
363
+ }
364
+
365
+ function bigrams(text) {
366
+ const value = String(text ?? '').toLowerCase().replace(/\s+/gu, ' ').trim();
367
+ const out = new Set();
368
+ for (let i = 0; i < value.length - 1; i += 1) out.add(value.slice(i, i + 2));
369
+ return out;
370
+ }
371
+
372
+ function cardScore(card, query) {
373
+ const q = String(query ?? '').trim().toLowerCase();
374
+ if (q === '') return 0;
375
+ const title = String(card.title ?? '').toLowerCase();
376
+ const terms = Array.isArray(card.terms) ? card.terms.join(' ').toLowerCase() : '';
377
+ let score = 0;
378
+ if (title.includes(q)) score += 5;
379
+ if (terms.includes(q)) score += 4;
380
+ const qb = bigrams(q);
381
+ const tb = bigrams(`${title} ${terms}`);
382
+ if (qb.size > 0) {
383
+ let hit = 0;
384
+ for (const gram of qb) if (tb.has(gram)) hit += 1;
385
+ score += (hit / qb.size) * 2;
386
+ }
387
+ return score;
388
+ }
389
+
390
+ /** Deterministic keyword/term search over trusted AutoCards. */
391
+ export function searchAutoCards({ memoryRoot, query, limit = 5, verified = true } = {}) {
392
+ const q = String(query ?? '').trim();
393
+ const max = Math.max(1, Math.min(20, Number(limit) || 5));
394
+ if (q === '') return [];
395
+ return (verified ? readVerifiedAutoCards({ memoryRoot }) : readAutoCards(memoryRoot))
396
+ .map((card) => ({ card, score: cardScore(card, q) }))
397
+ .filter((row) => row.score > 0)
398
+ .sort((a, b) => b.score - a.score || a.card.cardId.localeCompare(b.card.cardId))
399
+ .slice(0, max)
400
+ .map((row) => row.card);
401
+ }