@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,124 @@
1
+ // src/memory/review.js — M2-D:间隔重复(spaced repetition)调度
2
+ //
3
+ // 设计依据 §4.2/§4.5(review 调度 + promote 后可复习;不引第三方算法库,自研轻量版):
4
+ // * 固定阶梯 1/3/7/15/30/60/120 天(成功升一档、失败回到 1 天并降 strength);
5
+ // * dueAt = lastReviewedAt + intervalDays;dueRecords 只读;
6
+ // * recordReview 写 `.longlive/review/events.jsonl`(append-only)+ 1 行统一审计;
7
+ // 不改原文/卡文件,复习结果由上层按需 promote(decay.js)。
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ import { appendAuditRecord } from './audit-records.js';
13
+ import { ensureDir } from './state.js';
14
+
15
+ export const REVIEW_SCHEMA = 'review/v1';
16
+ export const REVIEW_INTERVALS_DAYS = Object.freeze([1, 3, 7, 15, 30, 60, 120]);
17
+ export const REVIEW_PASS_GRADE = 3;
18
+ const DAY_MS = 24 * 60 * 60 * 1000;
19
+
20
+ function iso(value) {
21
+ const date = value instanceof Date ? value : new Date(value);
22
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
23
+ }
24
+
25
+ export function reviewStateOf(record = {}) {
26
+ const review = record.review && typeof record.review === 'object' ? record.review : {};
27
+ const streak = Number.isInteger(review.streak) && review.streak >= 0 ? review.streak : 0;
28
+ const intervalDays = Number.isInteger(review.intervalDays) && review.intervalDays > 0
29
+ ? review.intervalDays
30
+ : REVIEW_INTERVALS_DAYS[0];
31
+ return {
32
+ streak,
33
+ intervalDays,
34
+ lastGrade: Number.isInteger(review.lastGrade) ? review.lastGrade : null,
35
+ lastReviewedAt: typeof review.lastReviewedAt === 'string' ? review.lastReviewedAt : null,
36
+ dueAt: typeof review.dueAt === 'string' ? review.dueAt : null,
37
+ };
38
+ }
39
+
40
+ /** Pure scheduling: next review time for a record, from its current review state. */
41
+ export function nextReviewAt(record = {}, now = new Date()) {
42
+ const state = reviewStateOf(record);
43
+ const base = state.lastReviewedAt ?? iso(now);
44
+ const time = Date.parse(base);
45
+ if (Number.isNaN(time)) return null;
46
+ const interval = REVIEW_INTERVALS_DAYS[Math.min(state.streak, REVIEW_INTERVALS_DAYS.length - 1)];
47
+ return new Date(time + interval * DAY_MS).toISOString();
48
+ }
49
+
50
+ export function isDue(record = {}, now = new Date()) {
51
+ const state = reviewStateOf(record);
52
+ if (state.dueAt === null) return true; // 未排期 => 到期(首次复习)
53
+ const due = Date.parse(state.dueAt);
54
+ return !Number.isNaN(due) && due <= now.getTime();
55
+ }
56
+
57
+ /** Due records, most overdue first (pure; skips superseded/archived). */
58
+ export function dueRecords(records = [], now = new Date()) {
59
+ return records
60
+ .filter((record) => record && record.state !== 'superseded' && record.state !== 'archived')
61
+ .filter((record) => isDue(record, now))
62
+ .map((record) => ({ id: record.id ?? null, state: reviewStateOf(record), dueAt: reviewStateOf(record).dueAt }))
63
+ .sort((left, right) => String(left.dueAt ?? '').localeCompare(String(right.dueAt ?? '')));
64
+ }
65
+
66
+ /**
67
+ * Pure grade: 0..5 (0 完全遗忘 … 5 完美). Success (>=3) raises streak/interval and
68
+ * strength; failure resets the ladder and lowers strength. Never below 0.
69
+ */
70
+ export function gradeReview(record = {}, grade, now = new Date()) {
71
+ const value = Math.max(0, Math.min(5, Number.isInteger(grade) ? grade : 0));
72
+ const state = reviewStateOf(record);
73
+ const success = value >= REVIEW_PASS_GRADE;
74
+ const streak = success ? state.streak + 1 : 0;
75
+ const intervalDays = REVIEW_INTERVALS_DAYS[Math.min(streak, REVIEW_INTERVALS_DAYS.length - 1)];
76
+ const lastReviewedAt = iso(now);
77
+ const dueAt = new Date(Date.parse(lastReviewedAt) + intervalDays * DAY_MS).toISOString();
78
+ const strength = Math.max(0, Math.min(9, (Number.isInteger(record?.strength) ? record.strength : 0) + (success ? 1 : -1)));
79
+ return {
80
+ grade: value,
81
+ success,
82
+ strength,
83
+ lastUsed: lastReviewedAt.slice(0, 10),
84
+ useCount: (Number.isInteger(record?.useCount) ? record.useCount : 0) + 1,
85
+ review: { streak, intervalDays, lastGrade: value, lastReviewedAt, dueAt },
86
+ };
87
+ }
88
+
89
+ /** Append one review event (append-only, never rewrites the source card/record). */
90
+ export function recordReview({ root, record, grade, now = new Date(), by = 'admin' } = {}) {
91
+ const memoryRoot = String(root ?? '');
92
+ if (memoryRoot === '') throw new Error('E_MEMORY_ROOT: recordReview requires root');
93
+ const id = String(record?.id ?? record?.rel ?? 'unknown');
94
+ const result = gradeReview(record, grade, now);
95
+ const dir = path.join(path.resolve(memoryRoot), '.longlive', 'review');
96
+ ensureDir(memoryRoot, dir);
97
+ const file = path.join(dir, 'events.jsonl');
98
+ const row = {
99
+ schema: REVIEW_SCHEMA,
100
+ id,
101
+ at: iso(now),
102
+ by,
103
+ grade: result.grade,
104
+ success: result.success,
105
+ review: result.review,
106
+ strength: result.strength,
107
+ useCount: result.useCount,
108
+ };
109
+ fs.appendFileSync(file, `${JSON.stringify(row)}\n`, 'utf8');
110
+ try {
111
+ appendAuditRecord({
112
+ memoryRoot,
113
+ action: 'review',
114
+ recordId: id,
115
+ readerTier: 'host',
116
+ questionId: 'unknown',
117
+ expectedTurns: null,
118
+ answerText: null,
119
+ at: iso(now),
120
+ extra: { grade: result.grade, streak: result.review.streak, dueAt: result.review.dueAt },
121
+ });
122
+ } catch { /* audit best-effort */ }
123
+ return { ok: true, schema: REVIEW_SCHEMA, id, file, result };
124
+ }
@@ -0,0 +1,88 @@
1
+ // src/memory/trust.js — 1.8.0 / v2.8 §15.1-A3:trustScore(规则写死,时间可注入)
2
+ //
3
+ // 公式(写死):
4
+ // w_tier = { user: 1.0, host: 0.9, agent: 0.6, plugin: 0.4 }
5
+ // trustScore = clamp(0, 1, w_tier * (1 + 0.1 * min(verifyCount, 5)) * decay)
6
+ // decay = 0.5 ^ (ageDays / 30) (ageDays 由注入的 now/createdAt 计算,不用真实等待)
7
+ //
8
+ // 时间注入 API(GOV-04:禁止 sleep 造假):
9
+ // - computeTrustScore({ sourceTier, verifyCount, ageDays })
10
+ // - trustScoreAt({ sourceTier, verifyCount, createdAt, now })
11
+ // - createGovTrustClock({ now }) → { at(createdAt), score(claim) } 测试显式传 now
12
+ // 低信任阈值 < 0.35 的 claim 只能进候选区、不得作为结论注入。
13
+ // legacy 迁移记录不做时间衰减(legacy:true → ageDays=0),避免把旧卡误判为低信任。
14
+
15
+ export const TRUST_TIERS = Object.freeze(['user', 'host', 'agent', 'plugin']);
16
+ export const TIER_WEIGHTS = Object.freeze({ user: 1.0, host: 0.9, agent: 0.6, plugin: 0.4 });
17
+ export const VERIFY_COUNT_CAP = 5;
18
+ export const LOW_TRUST_THRESHOLD = 0.35;
19
+ export const TRUST_SCHEMA = 'trust/v1';
20
+
21
+ function clamp01(value) {
22
+ if (!Number.isFinite(value)) return 0;
23
+ if (value < 0) return 0;
24
+ if (value > 1) return 1;
25
+ return value;
26
+ }
27
+
28
+ function toDateMs(value) {
29
+ if (value instanceof Date) return value.getTime();
30
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
31
+ if (typeof value === 'string' && value !== '') {
32
+ const parsed = Date.parse(value);
33
+ return Number.isFinite(parsed) ? parsed : null;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /** Pure formula (no clock): used directly by tests and by trustScoreAt. */
39
+ export function computeTrustScore({ sourceTier = 'agent', verifyCount = 0, ageDays = 0 } = {}) {
40
+ const tier = TRUST_TIERS.includes(sourceTier) ? sourceTier : 'agent';
41
+ const weight = TIER_WEIGHTS[tier];
42
+ const verified = Math.max(0, Math.min(VERIFY_COUNT_CAP, Number.isInteger(verifyCount) ? verifyCount : 0));
43
+ const days = Number.isFinite(ageDays) && ageDays > 0 ? ageDays : 0;
44
+ const decay = Math.pow(0.5, days / 30);
45
+ const score = weight * (1 + 0.1 * verified) * decay;
46
+ return {
47
+ schema: TRUST_SCHEMA,
48
+ sourceTier: tier,
49
+ verifyCount: verified,
50
+ ageDays: days,
51
+ decay,
52
+ trustScore: clamp01(score),
53
+ };
54
+ }
55
+
56
+ /** Time-injected score: age is computed from createdAt → now (both injectable). */
57
+ export function trustScoreAt({ sourceTier = 'agent', verifyCount = 0, createdAt = null, now = new Date(), legacy = false } = {}) {
58
+ const nowMs = toDateMs(now) ?? Date.now();
59
+ const createdMs = toDateMs(createdAt);
60
+ // A backwards clock must not inflate trust: negative age clamps to 0 (CLOCK_ROLLBACK-safe).
61
+ const rawAgeDays = createdMs === null ? 0 : (nowMs - createdMs) / (24 * 60 * 60 * 1000);
62
+ const ageDays = legacy === true ? 0 : (Number.isFinite(rawAgeDays) && rawAgeDays > 0 ? rawAgeDays : 0);
63
+ return computeTrustScore({ sourceTier, verifyCount, ageDays });
64
+ }
65
+
66
+ /** Time-injection seam for GOV-04 probes/tests (no sleep, no wall-clock dependency). */
67
+ export function createGovTrustClock({ now } = {}) {
68
+ const clockNow = () => (typeof now === 'function' ? now() : now ?? new Date());
69
+ return {
70
+ at: (createdAt) => trustScoreAt({ sourceTier: 'host', verifyCount: 0, createdAt, now: clockNow() }),
71
+ score: ({ sourceTier, verifyCount = 0, createdAt = null, legacy = false } = {}) => trustScoreAt({ sourceTier, verifyCount, createdAt, now: clockNow(), legacy }),
72
+ now: clockNow,
73
+ };
74
+ }
75
+
76
+ export function isLowTrust(trustScore) {
77
+ return !(typeof trustScore === 'number' && trustScore >= LOW_TRUST_THRESHOLD);
78
+ }
79
+
80
+ /** 低信任不进结论注入;候选区仍可见(标 unverified)。 */
81
+ export function injectionEligibility(claim = {}) {
82
+ const score = typeof claim.trustScore === 'number' ? claim.trustScore : 0;
83
+ return {
84
+ eligible: !isLowTrust(score),
85
+ unverified: isLowTrust(score),
86
+ trustScore: score,
87
+ };
88
+ }
@@ -0,0 +1,264 @@
1
+ // src/memory/version-chain.js — M2-D:蒸馏版本链 / active 指针 / 回滚 / 审计
2
+ //
3
+ // 设计依据 MEMORY-RECALL-V2-PROPOSAL §4.3/§4.4:
4
+ // * 每个蒸馏 id 一个 append-only JSONL(`.longlive/distill/<topic>-<period>.jsonl`),
5
+ // 一行一个版本,旧版本永久保留、绝不覆盖/删除;
6
+ // * `.longlive/distill/index.json` 只存 activeVersion 指针,回滚=切指针(ms 级);
7
+ // * 每次 append / rollback 写 1 行 `.longlive/distill.log`(JSON,可审计)+
8
+ // 1 行 `.longlive/audit/records.jsonl`(统一审计,A5 口径);
9
+ // * 本模块只做“链/指针/审计”原语,不做抽取、不做保护判定(见 distill.js / l2-gate.js)。
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ import { appendAuditRecord } from './audit-records.js';
15
+ import { memoryError } from './paths.js';
16
+ import { appendAudit, ensureDir, sha256Text, writeFileAtomic } from './state.js';
17
+
18
+ export const VERSION_CHAIN_SCHEMA = 'version-chain/v1';
19
+ export const DISTILL_DIR = '.longlive/distill';
20
+ export const DISTILL_INDEX_FILE = '.longlive/distill/index.json';
21
+ export const DISTILL_LOG_FILE = '.longlive/distill.log';
22
+ export const DISTILL_INDEX_SCHEMA = 'distill-index/v1';
23
+
24
+ export function distillDirOf(root) {
25
+ return path.join(path.resolve(String(root)), DISTILL_DIR);
26
+ }
27
+
28
+ export function chainIndexPathOf(root) {
29
+ return path.join(path.resolve(String(root)), DISTILL_INDEX_FILE);
30
+ }
31
+
32
+ export function chainLogPathOf(root) {
33
+ return path.join(path.resolve(String(root)), DISTILL_LOG_FILE);
34
+ }
35
+
36
+ /** File name for one chain: only [A-Za-z0-9._-] survives (path traversal impossible). */
37
+ export function chainFileName({ topic, period }) {
38
+ const slug = (value) => String(value ?? 'unknown')
39
+ .normalize('NFKC')
40
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
41
+ .replace(/^-+|-+$/g, '')
42
+ .slice(0, 80) || 'unknown';
43
+ return `${slug(topic)}-${slug(period)}.jsonl`;
44
+ }
45
+
46
+ export function chainFilePathOf(root, id, file = null) {
47
+ const name = typeof file === 'string' && file !== '' ? file : chainFileName({ topic: id, period: 'chain' });
48
+ return path.join(distillDirOf(root), path.basename(name));
49
+ }
50
+
51
+ /** Deterministic JSON: object keys sorted recursively (idempotency hashes). */
52
+ export function stableStringify(value) {
53
+ if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(',')}]`;
54
+ if (value && typeof value === 'object') {
55
+ const keys = Object.keys(value).sort();
56
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
57
+ }
58
+ return JSON.stringify(value === undefined ? null : value);
59
+ }
60
+
61
+ export function contentHashOf(value) {
62
+ return sha256Text(stableStringify(value));
63
+ }
64
+
65
+ export function isEmptyIndex() {
66
+ return { schema: DISTILL_INDEX_SCHEMA, version: 1, entries: {} };
67
+ }
68
+
69
+ /** Read the active-pointer index; missing file => empty index, corrupt file => refuse (fail-safe). */
70
+ export function readChainIndex(root) {
71
+ const file = chainIndexPathOf(root);
72
+ let raw;
73
+ try {
74
+ raw = fs.readFileSync(file, 'utf8');
75
+ } catch (error) {
76
+ if (error?.code === 'ENOENT') return { ...isEmptyIndex(), file };
77
+ throw memoryError('E_CHAIN_INDEX_READ', `读不到 ${file}`, error);
78
+ }
79
+ let parsed;
80
+ try {
81
+ parsed = JSON.parse(raw);
82
+ } catch (error) {
83
+ throw memoryError('E_CHAIN_INDEX_CORRUPT', `版本索引损坏(拒绝覆盖,先人工核对):${file}`, error);
84
+ }
85
+ const entries = parsed && typeof parsed === 'object' && parsed.entries && typeof parsed.entries === 'object'
86
+ ? parsed.entries
87
+ : {};
88
+ return { schema: parsed?.schema ?? DISTILL_INDEX_SCHEMA, version: parsed?.version ?? 1, entries, file };
89
+ }
90
+
91
+ export function readIndexEntry(root, id) {
92
+ const index = readChainIndex(root);
93
+ const entry = index.entries[String(id)];
94
+ if (!entry || typeof entry !== 'object') return null;
95
+ return {
96
+ id: String(id),
97
+ file: typeof entry.file === 'string' ? entry.file : null,
98
+ activeVersion: Number.isInteger(entry.activeVersion) ? entry.activeVersion : null,
99
+ versionCount: Number.isInteger(entry.versionCount) ? entry.versionCount : null,
100
+ contentHash: typeof entry.contentHash === 'string' ? entry.contentHash : null,
101
+ updatedAt: typeof entry.updatedAt === 'string' ? entry.updatedAt : null,
102
+ };
103
+ }
104
+
105
+ export function listChains(root) {
106
+ const index = readChainIndex(root);
107
+ return Object.keys(index.entries).sort().map((id) => ({
108
+ id,
109
+ ...readIndexEntry(root, id),
110
+ }));
111
+ }
112
+
113
+ /** Parse one chain file into versions (append-only order). Corrupt line => refuse. */
114
+ export function readVersions(file) {
115
+ let raw;
116
+ try {
117
+ raw = fs.readFileSync(file, 'utf8');
118
+ } catch (error) {
119
+ if (error?.code === 'ENOENT') return [];
120
+ throw memoryError('E_CHAIN_READ', `读不到版本链:${file}`, error);
121
+ }
122
+ const versions = [];
123
+ raw.split(/\r?\n/).forEach((line, index) => {
124
+ if (line.trim() === '') return;
125
+ try {
126
+ const row = JSON.parse(line);
127
+ if (!row || typeof row !== 'object' || !Number.isInteger(row.version)) {
128
+ throw new Error('version is not an integer');
129
+ }
130
+ versions.push(row);
131
+ } catch (error) {
132
+ throw memoryError('E_CHAIN_CORRUPT', `版本链第 ${index + 1} 行损坏:${file}`, error);
133
+ }
134
+ });
135
+ return versions;
136
+ }
137
+
138
+ /** Append one version line (fsync; never rewrites existing bytes). */
139
+ export function appendVersionLine({ file, record }) {
140
+ if (!record || !Number.isInteger(record.version) || record.version < 1) {
141
+ throw memoryError('E_CHAIN_VERSION', 'appendVersionLine requires record.version >= 1');
142
+ }
143
+ fs.mkdirSync(path.dirname(file), { recursive: true });
144
+ const fd = fs.openSync(file, 'a');
145
+ try {
146
+ fs.writeSync(fd, `${JSON.stringify(record)}\n`, null, 'utf8');
147
+ fs.fsyncSync(fd);
148
+ } finally {
149
+ fs.closeSync(fd);
150
+ }
151
+ return { file, version: record.version, line: JSON.stringify(record) };
152
+ }
153
+
154
+ /** Atomically set the active version pointer for one chain. */
155
+ export function setActiveVersion({ root, id, file, version, versionCount, contentHash = null, now = new Date() }) {
156
+ const index = readChainIndex(root);
157
+ index.entries[String(id)] = {
158
+ file: path.basename(String(file)),
159
+ activeVersion: version,
160
+ versionCount,
161
+ contentHash,
162
+ updatedAt: now.toISOString(),
163
+ };
164
+ const next = { schema: DISTILL_INDEX_SCHEMA, version: 1, entries: index.entries, updatedAt: now.toISOString() };
165
+ writeFileAtomic(root, chainIndexPathOf(root), `${JSON.stringify(next, null, 2)}\n`, { backup: false, now });
166
+ return { file: chainIndexPathOf(root), entry: index.entries[String(id)] };
167
+ }
168
+
169
+ /** Append one structured audit line to `.longlive/distill.log` (JSONL). */
170
+ export function appendChainLog({ root, op, id = null, file = null, fromVersion = null, toVersion = null, by = 'admin', reason = '', contentHash = null, now = new Date() }) {
171
+ const row = {
172
+ schema: 'distill-log/v1',
173
+ at: now.toISOString(),
174
+ op,
175
+ id,
176
+ file: file === null ? null : path.basename(String(file)),
177
+ fromVersion,
178
+ toVersion,
179
+ by,
180
+ reason,
181
+ contentHash,
182
+ };
183
+ const target = chainLogPathOf(root);
184
+ ensureDir(root, path.dirname(target));
185
+ fs.appendFileSync(target, `${JSON.stringify(row)}\n`, 'utf8');
186
+ return { file: target, row };
187
+ }
188
+
189
+ function appendUnifiedAudit({ root, action, recordId, extra, now }) {
190
+ try {
191
+ appendAuditRecord({
192
+ memoryRoot: root,
193
+ action,
194
+ recordId,
195
+ readerTier: 'host',
196
+ questionId: 'unknown',
197
+ expectedTurns: null,
198
+ answerText: null,
199
+ at: now.toISOString(),
200
+ extra,
201
+ });
202
+ } catch {
203
+ /* 统一审计是 best-effort:链日志已写,不能让审计写失败吞掉回滚结果 */
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Roll one chain back to a concrete version: only the active pointer moves;
209
+ * every version row stays on disk. Writes 1 distill.log row + 1 unified audit row.
210
+ */
211
+ export function rollbackVersion({ root, id, toVersion, by = 'admin', reason = '', now = new Date() }) {
212
+ const entry = readIndexEntry(root, id);
213
+ if (!entry || !entry.file || entry.activeVersion === null) {
214
+ throw memoryError('E_CHAIN_NOT_FOUND', `没有这个蒸馏 id(或尚未建链):${id}`);
215
+ }
216
+ const file = chainFilePathOf(root, id, entry.file);
217
+ const versions = readVersions(file);
218
+ const target = versions.find((row) => row.version === toVersion);
219
+ if (!target) {
220
+ throw memoryError('E_CHAIN_NO_VERSION', `链 ${id} 没有 v${toVersion}(现有 v${versions[0]?.version}..v${versions.at(-1)?.version})`);
221
+ }
222
+ if (entry.activeVersion === toVersion) {
223
+ return { ok: true, changed: false, id, file, fromVersion: entry.activeVersion, toVersion, versions: versions.length };
224
+ }
225
+ const fromVersion = entry.activeVersion;
226
+ setActiveVersion({
227
+ root,
228
+ id,
229
+ file,
230
+ version: toVersion,
231
+ versionCount: versions.length,
232
+ contentHash: target.contentHash ?? null,
233
+ now,
234
+ });
235
+ appendChainLog({ root, op: 'rollback', id, file, fromVersion, toVersion, by, reason, contentHash: target.contentHash ?? null, now });
236
+ appendAudit(root, { action: 'memory_distill_rollback', object: `${id}@v${fromVersion}->v${toVersion}`, result: `by=${by} reason=${reason || '-'}`, now });
237
+ appendUnifiedAudit({
238
+ root,
239
+ action: 'distill_rollback',
240
+ recordId: id,
241
+ extra: { fromVersion, toVersion, by, reason, file: path.basename(file) },
242
+ now,
243
+ });
244
+ return { ok: true, changed: true, id, file, fromVersion, toVersion, versions: versions.length };
245
+ }
246
+
247
+ /** Field-level diff between two version rows (audit/createdAt excluded). */
248
+ export function diffVersions(from, to) {
249
+ const changed = [];
250
+ const keys = new Set([...Object.keys(from ?? {}), ...Object.keys(to ?? {})]);
251
+ for (const key of [...keys].sort()) {
252
+ if (key === 'audit' || key === 'contentHash') continue;
253
+ const left = stableStringify(from?.[key] ?? null);
254
+ const right = stableStringify(to?.[key] ?? null);
255
+ if (left !== right) changed.push({ field: key, from: from?.[key] ?? null, to: to?.[key] ?? null });
256
+ }
257
+ return {
258
+ changed,
259
+ fromVersion: from?.version ?? null,
260
+ toVersion: to?.version ?? null,
261
+ fromGist: from?.gist ?? null,
262
+ toGist: to?.gist ?? null,
263
+ };
264
+ }
@@ -0,0 +1,137 @@
1
+ // src/memory/wal.js — M2-B / §2.3:索引写入的 WAL(崩溃安全 + 可重放)
2
+ //
3
+ // 写路径:先 append 一条 pending intent → 执行真正的写入(DB 事务)→ append done。
4
+ // 进程中途崩溃时,下次启动 `replay()` 把 pending 条目重新执行(写入本身幂等),
5
+ // 所以不会出现「半行索引」。`compact()` 只保留未完成条目,控制文件体积。
6
+ //
7
+ // 文件:<memoryRoot>/.longlive/index/wal.jsonl
8
+ // 行:{schema:'wal/v1', walId, op, at, status:'pending'|'done'|'failed', payload, error?}
9
+
10
+ import crypto from 'node:crypto';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+
14
+ import { stableStringify } from '../codec.js';
15
+
16
+ export const WAL_SCHEMA = 'wal/v1';
17
+ export const WAL_MAX_BYTES = 2 * 1024 * 1024;
18
+
19
+ export function walPathOf(memoryRoot) {
20
+ return path.join(path.resolve(String(memoryRoot)), '.longlive', 'index', 'wal.jsonl');
21
+ }
22
+
23
+ export function walIdOf(entry) {
24
+ return crypto.createHash('sha1').update(stableStringify(entry)).digest('hex').slice(0, 16);
25
+ }
26
+
27
+ function nowMs(now) {
28
+ const value = typeof now === 'function' ? now() : new Date();
29
+ return value instanceof Date ? value.getTime() : Number(value);
30
+ }
31
+
32
+ export function createWal(options = {}) {
33
+ const memoryRoot = path.resolve(String(options.memoryRoot ?? ''));
34
+ const file = options.walPath ? path.resolve(options.walPath) : walPathOf(memoryRoot);
35
+ const now = options.now;
36
+ fs.mkdirSync(path.dirname(file), { recursive: true });
37
+
38
+ function append(line) {
39
+ fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
40
+ return line;
41
+ }
42
+
43
+ /** Record an intent; returns the walId used to mark completion. */
44
+ function begin(op, payload, extra = {}) {
45
+ const entry = { op, payload };
46
+ const walId = options.walId?.(entry) ?? walIdOf(entry);
47
+ append({ schema: WAL_SCHEMA, walId, op, at: nowMs(now), status: 'pending', payload, ...extra });
48
+ return walId;
49
+ }
50
+
51
+ function markDone(walId, extra = {}) {
52
+ append({ schema: WAL_SCHEMA, walId, at: nowMs(now), status: 'done', ...extra });
53
+ return { ok: true, walId };
54
+ }
55
+
56
+ function markFailed(walId, error, extra = {}) {
57
+ append({ schema: WAL_SCHEMA, walId, at: nowMs(now), status: 'failed', error: String(error ?? 'unknown'), ...extra });
58
+ return { ok: true, walId };
59
+ }
60
+
61
+ /** Fold the log: last status wins. */
62
+ function fold() {
63
+ const entries = new Map();
64
+ let text = '';
65
+ try {
66
+ text = fs.readFileSync(file, 'utf8');
67
+ } catch {
68
+ return entries;
69
+ }
70
+ for (const line of text.split('\n')) {
71
+ if (line.trim() === '') continue;
72
+ let row = null;
73
+ try {
74
+ row = JSON.parse(line);
75
+ } catch {
76
+ continue;
77
+ }
78
+ if (typeof row?.walId !== 'string') continue;
79
+ const previous = entries.get(row.walId) ?? {};
80
+ entries.set(row.walId, { ...previous, ...row, status: row.status ?? previous.status ?? 'pending' });
81
+ }
82
+ return entries;
83
+ }
84
+
85
+ function pending() {
86
+ return [...fold().values()].filter((entry) => entry.status === 'pending');
87
+ }
88
+
89
+ function all() {
90
+ return [...fold().values()];
91
+ }
92
+
93
+ function failed() {
94
+ return [...fold().values()].filter((entry) => entry.status === 'failed');
95
+ }
96
+
97
+ /**
98
+ * Replay every pending intent through `handler(entry)`. Handler must be idempotent.
99
+ * Returns {ok, replayed, failed}.
100
+ */
101
+ function replay(handler) {
102
+ if (typeof handler !== 'function') throw new Error('wal.replay requires handler(entry)');
103
+ const results = { ok: true, replayed: 0, failed: 0, errors: [] };
104
+ for (const entry of pending()) {
105
+ try {
106
+ handler(entry);
107
+ markDone(entry.walId, { replayed: true });
108
+ results.replayed += 1;
109
+ } catch (error) {
110
+ markFailed(entry.walId, error?.message ?? error);
111
+ results.failed += 1;
112
+ results.errors.push({ walId: entry.walId, op: entry.op, message: String(error?.message ?? error) });
113
+ }
114
+ }
115
+ results.ok = results.failed === 0;
116
+ return results;
117
+ }
118
+
119
+ /** Rewrite only pending lines (atomic tmp→rename); completed/failed rows dropped. */
120
+ function compact() {
121
+ const keep = pending();
122
+ const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
123
+ fs.writeFileSync(tmp, keep.map((entry) => JSON.stringify(entry)).join('\n') + (keep.length > 0 ? '\n' : ''), 'utf8');
124
+ fs.renameSync(tmp, file);
125
+ return { ok: true, kept: keep.length };
126
+ }
127
+
128
+ function size() {
129
+ try {
130
+ return fs.statSync(file).size;
131
+ } catch {
132
+ return 0;
133
+ }
134
+ }
135
+
136
+ return { file, begin, markDone, markFailed, pending, failed, all, replay, compact, size, fold };
137
+ }