@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24

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 (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,3 +1,23 @@
1
+ /**
2
+ * 事务化状态账本(ADR-15,docs/rfc/transactional-state-rfc.md):
3
+ * 「文件即权威」→「单文件 SQLite 账本即权威」。
4
+ *
5
+ * - events 表 = 唯一权威(append-only;语义幂等键物理化为 UNIQUE 索引)
6
+ * - 投影表(projection_docs/items)= 日志的确定性 fold,可随时 DROP 重建
7
+ * - 红线进事务:evidence/conditions 校验发生在写事务内,拒绝即回滚账本无痕
8
+ * - 一次写 = 单事务{INSERT event; UPDATE 投影},跨文件分叉在物理上不可能
9
+ * - 语义层零改造:mergeProfile/appendTrip/upsertCompanion/appendEvent 纯函数
10
+ * 原样复用为写路径守门与 fold 处理器(memory-design §1.6「账本化只换存储面」)
11
+ *
12
+ * 迁移纪律(D2 one-shot):旧 JSON/JSONL 文件在首次写时自动导入为 events
13
+ * (单事务 + 导入前快照到 gotry-state/pre-ledger-backup/);此后文件降级为
14
+ * 导出视图(state-cli export),永不再回流。读路径带文件回退(未迁移 root
15
+ * 仍可读旧文件),写路径必经账本——工具外无写入路径。
16
+ *
17
+ * 引擎:better-sqlite3(复用矩阵 open-source import 通道),WAL + NORMAL,
18
+ * 同步 API 与仓内 readFileSync 风格一致;busy_timeout 容忍跨进程
19
+ * (dsh 运行时 / async-collect / state-cli tick 并存)。
20
+ */
1
21
  import Database from 'better-sqlite3';
2
22
  import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
3
23
  import { createHash } from 'node:crypto';
@@ -6,13 +26,15 @@ import { mergeProfile } from './memory-capture.js';
6
26
  import { appendEvent as gateUtilityEvent, projectUtility } from './memory-utility.js';
7
27
  import { appendTrip as gateTrip } from './travel-timeline.js';
8
28
  import { upsertCompanion } from './companions.js';
9
- const SCHEMA_VERSION = '1';
29
+ const SCHEMA_VERSION = '2';
30
+ /** 稳定 wish 主键:名称语义派生(重放/同名幂等更新不再依赖墙钟;RFC §1.1 id 不稳定缺口的修复) */
10
31
  export function makeWishId(name) {
11
32
  return 'w-' + createHash('sha256').update(String(name).trim()).digest('hex').slice(0, 10);
12
33
  }
13
34
  function sha(s) {
14
35
  return createHash('sha256').update(s).digest('hex');
15
36
  }
37
+ /** stateRoot 可相对可绝对(与 incident-log.resolveIncidentsPath 同款解析) */
16
38
  function stateDirOf(stateRoot) {
17
39
  const root = isAbsolute(stateRoot) ? stateRoot : join(process.cwd(), stateRoot);
18
40
  return join(root, 'gotry-state');
@@ -23,16 +45,10 @@ export function ledgerDbPath(stateRoot) {
23
45
  export function ledgerExists(stateRoot) {
24
46
  return existsSync(ledgerDbPath(stateRoot));
25
47
  }
26
- const LEGACY_FILES = [
27
- 'motivation-profile.json',
28
- 'wish-pool.json',
29
- 'memory-utility.jsonl',
30
- 'trips.jsonl',
31
- 'companions.json'
32
- ];
48
+ const LEGACY_FILES = ['motivation-profile.json', 'wish-pool.json', 'memory-utility.jsonl', 'trips.jsonl', 'companions.json'];
33
49
  function legacyFilesPresent(stateRoot) {
34
50
  const dir = stateDirOf(stateRoot);
35
- return LEGACY_FILES.some((f)=>existsSync(join(dir, f)));
51
+ return LEGACY_FILES.some(f => existsSync(join(dir, f)));
36
52
  }
37
53
  function isUniqueViolation(e) {
38
54
  return e instanceof Error && e.code === 'SQLITE_CONSTRAINT_UNIQUE';
@@ -108,101 +124,86 @@ export class StateLedger {
108
124
  stateRoot;
109
125
  tenant;
110
126
  dbPath;
111
- constructor(db, stateRoot, tenant = 'local'){
127
+ constructor(db, stateRoot, tenant = 'local') {
112
128
  this.db = db;
113
129
  this.stateRoot = stateRoot;
114
130
  this.tenant = tenant;
115
131
  this.dbPath = ledgerDbPath(stateRoot);
116
132
  }
133
+ // ---- 基础事件面 ------------------------------------------------------------
134
+ /** 事件插入(idem 命中 → 返回 null,不抛;红线校验异常向上抛 = 事务回滚) */
117
135
  insertEvent(ev) {
118
- const stmt = this.db.prepare(`INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id)
119
- VALUES (?, ?, ?, ?, ?, ?, ?)`);
136
+ const stmt = this.db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
137
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
120
138
  try {
121
- const info = stmt.run(ev.ts ?? new Date().toISOString(), ev.actor, ev.kind, ev.subjectId ?? '', JSON.stringify(ev.payload), ev.idemKey ?? null, ev.runId ?? null);
139
+ const info = stmt.run(this.tenant, ev.ts ?? new Date().toISOString(), ev.actor, ev.kind, ev.subjectId ?? '', JSON.stringify(ev.payload), ev.idemKey ?? null, ev.runId ?? null);
122
140
  return Number(info.lastInsertRowid);
123
- } catch (e) {
124
- if (isUniqueViolation(e)) return null;
141
+ }
142
+ catch (e) {
143
+ if (isUniqueViolation(e))
144
+ return null;
125
145
  throw e;
126
146
  }
127
147
  }
128
148
  readEvents(kind, limit = 100) {
129
- const where = kind ? 'WHERE kind = ?' : '';
130
- const rows = this.db.prepare(`SELECT seq, ts, actor, kind, subject_id, payload, idem_key, run_id
131
- FROM events ${where} ORDER BY seq DESC LIMIT ?`).all(...kind ? [
132
- kind,
133
- limit
134
- ] : [
135
- limit
136
- ]);
149
+ const rows = kind
150
+ ? this.db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
151
+ FROM events WHERE tenant_id = ? AND kind = ? ORDER BY seq DESC LIMIT ?`).all(this.tenant, kind, limit)
152
+ : this.db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
153
+ FROM events WHERE tenant_id = ? ORDER BY seq DESC LIMIT ?`).all(this.tenant, limit);
137
154
  return rows;
138
155
  }
139
156
  countEvents() {
140
157
  return this.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id = ?').get(this.tenant).n;
141
158
  }
159
+ // ---- 读:投影与日志类事件 ----------------------------------------------------
142
160
  readMotivation() {
143
161
  const row = this.db.prepare(`SELECT doc FROM projection_docs WHERE subject = 'motivation' AND tenant_id = ?`).get(this.tenant);
144
162
  return row ? JSON.parse(row.doc) : null;
145
163
  }
146
164
  readWishPool() {
147
165
  const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ? ORDER BY ord, rowid`).all(this.tenant);
148
- return rows.map((r)=>JSON.parse(r.doc));
166
+ return rows.map(r => JSON.parse(r.doc));
149
167
  }
150
168
  readCompanions() {
151
169
  const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'companions' AND tenant_id = ? ORDER BY ord, rowid`).all(this.tenant);
152
- return rows.map((r)=>JSON.parse(r.doc));
170
+ return rows.map(r => JSON.parse(r.doc));
153
171
  }
154
172
  readUtilityEvents() {
155
173
  const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'memory_utility.event' AND tenant_id = ? ORDER BY seq`).all(this.tenant);
156
- return rows.map((r)=>JSON.parse(r.payload).event);
174
+ return rows.map(r => JSON.parse(r.payload).event);
157
175
  }
158
176
  readTrips() {
159
177
  const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'trip.logged' AND tenant_id = ? ORDER BY seq`).all(this.tenant);
160
- return rows.map((r)=>JSON.parse(r.payload).trip);
178
+ return rows.map(r => JSON.parse(r.payload).trip);
161
179
  }
180
+ /** 效用投影(与文件版 projectUtility 同源同构) */
162
181
  projectUtilityNow() {
163
182
  return projectUtility(this.readUtilityEvents());
164
183
  }
184
+ // ---- 写:单事务{INSERT event; UPDATE 投影},守门纯函数原样复用 -----------------
185
+ // 红线进事务:evidence 缺失/conditions 空 → mergeProfile/校验拒绝 → 事务回滚,账本无痕。
186
+ /** 动机画像增量补丁(守门 = mergeProfile:追加不删史/幂等/权重变更须伴新证据) */
165
187
  appendMotivationPatch(patch, actor = 'tool:gotry_motivation_save') {
188
+ // 事件边界 ISO:事件行的 ts 就是 homeCityPreference.updated_at 的源头,
189
+ // fold 路径会读同一字段回放——rebuildProjections 必须能复现该值。
166
190
  const ts = new Date().toISOString();
167
- const run = this.db.transaction(()=>{
191
+ const run = this.db.transaction(() => {
168
192
  const current = this.readMotivation();
169
- const merged = mergeProfile(current, patch);
193
+ const merged = mergeProfile(current, patch, ts);
170
194
  if (!merged) {
171
- const unchanged = {
172
- ...current ?? {
173
- weights: {},
174
- evidence: [],
175
- hard: {}
176
- },
177
- updated_at: current?.updated_at ?? ts
178
- };
179
- return {
180
- saved: false,
181
- profile: unchanged
182
- };
195
+ const unchanged = { ...(current ?? { weights: {}, evidence: [], hard: {} }), updated_at: current?.updated_at ?? ts };
196
+ return { saved: false, profile: unchanged };
183
197
  }
184
- const doc = {
185
- ...merged,
186
- updated_at: ts
187
- };
188
- this.insertEvent({
189
- actor,
190
- kind: 'motivation.patch',
191
- subjectId: 'motivation',
192
- payload: {
193
- patch
194
- },
195
- ts
196
- });
198
+ const doc = { ...merged, updated_at: ts };
199
+ this.insertEvent({ actor, kind: 'motivation.patch', subjectId: 'motivation', payload: { patch }, ts });
197
200
  this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
198
201
  ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
199
- return {
200
- saved: true,
201
- profile: doc
202
- };
202
+ return { saved: true, profile: doc };
203
203
  });
204
204
  return run();
205
205
  }
206
+ /** 愿望入池/同名幂等更新(红线:conditions 必填非空) */
206
207
  appendWish(entry, actor = 'tool:gotry_wish_pool_add') {
207
208
  const name = String(entry.name ?? '').trim();
208
209
  const conditions = entry.conditions;
@@ -210,9 +211,9 @@ export class StateLedger {
210
211
  throw new Error('wish pool entry requires name and conditions (fulfilment conditions are the whole point)');
211
212
  }
212
213
  const ts = new Date().toISOString();
213
- const run = this.db.transaction(()=>{
214
+ const run = this.db.transaction(() => {
214
215
  const pool = this.readWishPool();
215
- const existing = pool.find((w)=>String(w.name ?? '') === name);
216
+ const existing = pool.find(w => String(w.name ?? '') === name);
216
217
  if (existing) {
217
218
  const id = typeof existing.wish_id === 'string' && existing.wish_id ? existing.wish_id : makeWishId(name);
218
219
  const next = {
@@ -220,32 +221,11 @@ export class StateLedger {
220
221
  wish_id: id,
221
222
  reason: entry.reason !== undefined ? entry.reason : existing.reason,
222
223
  conditions,
223
- ...entry.muted !== undefined ? {
224
- muted: Boolean(entry.muted)
225
- } : {}
224
+ ...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}),
226
225
  };
227
- this.insertEvent({
228
- actor,
229
- kind: 'wish.updated',
230
- subjectId: id,
231
- payload: {
232
- wish_id: id,
233
- patch: {
234
- reason: next.reason,
235
- conditions,
236
- ...entry.muted !== undefined ? {
237
- muted: Boolean(entry.muted)
238
- } : {}
239
- }
240
- },
241
- ts
242
- });
226
+ this.insertEvent({ actor, kind: 'wish.updated', subjectId: id, payload: { wish_id: id, patch: { reason: next.reason, conditions, ...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}) } }, ts });
243
227
  this.upsertItem('wish_pool', id, next);
244
- return {
245
- added: false,
246
- wish_id: id,
247
- total: pool.length
248
- };
228
+ return { added: false, wish_id: id, total: pool.length };
249
229
  }
250
230
  const id = makeWishId(name);
251
231
  const doc = {
@@ -253,158 +233,80 @@ export class StateLedger {
253
233
  name,
254
234
  reason: entry.reason ?? '',
255
235
  conditions,
256
- ...entry.muted !== undefined ? {
257
- muted: Boolean(entry.muted)
258
- } : {},
259
- added_at: ts
236
+ ...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}),
237
+ added_at: ts,
260
238
  };
261
- const seq = this.insertEvent({
262
- actor,
263
- kind: 'wish.added',
264
- subjectId: id,
265
- payload: {
266
- wish: doc
267
- },
268
- ts
269
- });
239
+ const seq = this.insertEvent({ actor, kind: 'wish.added', subjectId: id, payload: { wish: doc }, ts });
270
240
  this.insertItem('wish_pool', id, doc, seq ?? 0);
271
- return {
272
- added: true,
273
- wish_id: id,
274
- total: pool.length + 1
275
- };
241
+ return { added: true, wish_id: id, total: pool.length + 1 };
276
242
  });
277
243
  return run();
278
244
  }
245
+ /** 效用事件(幂等键 = 语义 event_id;verified 无归因自动降级 applied——纯函数同款纪律) */
279
246
  appendUtilityEvent(ev, actor = 'tool:gotry_wish_pool_list') {
280
247
  const full = gateUtilityEvent([], ev).events[0];
281
- if (!full) return {
282
- appended: false,
283
- events: this.readUtilityEvents()
284
- };
285
- const run = this.db.transaction(()=>{
286
- return this.insertEvent({
287
- actor,
288
- kind: 'memory_utility.event',
289
- subjectId: full.wish_id,
290
- payload: {
291
- event: full
292
- },
293
- idemKey: `mu:${full.event_id}`
294
- }) !== null;
248
+ if (!full)
249
+ return { appended: false, events: this.readUtilityEvents() };
250
+ const run = this.db.transaction(() => {
251
+ return this.insertEvent({ actor, kind: 'memory_utility.event', subjectId: full.wish_id, payload: { event: full }, idemKey: `mu:${full.event_id}` }) !== null;
295
252
  });
296
- return {
297
- appended: run(),
298
- events: this.readUtilityEvents()
299
- };
253
+ return { appended: run(), events: this.readUtilityEvents() };
300
254
  }
255
+ /** 时间线行程(守门 = appendTrip:必填/绝对日期/重叠冲突即停;幂等键 = trip_id) */
301
256
  appendTripEvent(ev, actor = 'tool:gotry_trip_log') {
302
- const run = this.db.transaction(()=>{
257
+ const run = this.db.transaction(() => {
303
258
  const existing = this.readTrips();
304
259
  const gate = gateTrip(existing, ev);
305
- if (!gate.appended) return {
306
- appended: false,
307
- tripId: gate.tripId,
308
- reason: gate.reason,
309
- total: existing.length
310
- };
260
+ if (!gate.appended)
261
+ return { appended: false, tripId: gate.tripId, reason: gate.reason, total: existing.length };
311
262
  const full = gate.events[gate.events.length - 1];
312
- this.insertEvent({
313
- actor,
314
- kind: 'trip.logged',
315
- subjectId: full.trip_id,
316
- payload: {
317
- trip: full
318
- },
319
- idemKey: `trip:${full.trip_id}`
320
- });
321
- return {
322
- appended: true,
323
- tripId: full.trip_id,
324
- total: gate.events.length
325
- };
263
+ this.insertEvent({ actor, kind: 'trip.logged', subjectId: full.trip_id, payload: { trip: full }, idemKey: `trip:${full.trip_id}` });
264
+ return { appended: true, tripId: full.trip_id, total: gate.events.length };
326
265
  });
327
266
  return run();
328
267
  }
268
+ /** 同行人档案(守门 = upsertCompanion:负面清单拒收/追加不删史/幂等) */
329
269
  appendCompanion(patch, actor = 'tool:gotry_companion_save') {
330
- const run = this.db.transaction(()=>{
270
+ const run = this.db.transaction(() => {
331
271
  const profiles = this.readCompanions();
332
272
  const res = upsertCompanion(profiles, patch);
333
- if (!res.appended) return {
334
- appended: false,
335
- companionId: res.companionId,
336
- reason: res.reason,
337
- total: profiles.length
338
- };
339
- this.insertEvent({
340
- actor,
341
- kind: 'companion.saved',
342
- subjectId: res.companionId,
343
- payload: {
344
- patch
345
- }
346
- });
347
- this.replaceItems('companions', res.profiles.map((p)=>[
348
- p.companion_id,
349
- p
350
- ]));
351
- return {
352
- appended: true,
353
- companionId: res.companionId,
354
- total: res.profiles.length
355
- };
273
+ if (!res.appended)
274
+ return { appended: false, companionId: res.companionId, reason: res.reason, total: profiles.length };
275
+ this.insertEvent({ actor, kind: 'companion.saved', subjectId: res.companionId, payload: { patch } });
276
+ this.replaceItems('companions', res.profiles.map(p => [p.companion_id, p]));
277
+ return { appended: true, companionId: res.companionId, total: res.profiles.length };
356
278
  });
357
279
  return run();
358
280
  }
281
+ /**
282
+ * 成行确认(原跨两文件写 → 单事务):verified_outcome 效用事件 + 可选时间线行程,
283
+ * 同事务提交,崩溃后要么全有要么全无(RFC §1.1 partial-writes 缺口的修复)。
284
+ */
359
285
  confirmOutcome(input, actor = 'tool:gotry_wish_pool_list') {
360
- const run = this.db.transaction(()=>{
286
+ const run = this.db.transaction(() => {
361
287
  const now = new Date().toISOString();
362
288
  const full = gateUtilityEvent([], {
363
- wish_id: input.wishId,
364
- kind: 'verified_outcome',
365
- ts: now,
366
- ctx: 'gotry_wish_pool_list.confirm',
367
- detail: input.detail,
368
- attribution: input.attribution
289
+ wish_id: input.wishId, kind: 'verified_outcome', ts: now,
290
+ ctx: 'gotry_wish_pool_list.confirm', detail: input.detail, attribution: input.attribution,
369
291
  }).events[0];
370
- const recorded = full ? this.insertEvent({
371
- actor,
372
- kind: 'memory_utility.event',
373
- subjectId: full.wish_id,
374
- payload: {
375
- event: full
376
- },
377
- idemKey: `mu:${full.event_id}`
378
- }) !== null : false;
292
+ const recorded = full
293
+ ? this.insertEvent({ actor, kind: 'memory_utility.event', subjectId: full.wish_id, payload: { event: full }, idemKey: `mu:${full.event_id}` }) !== null
294
+ : false;
379
295
  let trip;
380
296
  if (input.trip) {
381
297
  const existing = this.readTrips();
382
298
  const gate = gateTrip(existing, input.trip);
383
299
  if (gate.appended) {
384
300
  const fullTrip = gate.events[gate.events.length - 1];
385
- this.insertEvent({
386
- actor,
387
- kind: 'trip.logged',
388
- subjectId: fullTrip.trip_id,
389
- payload: {
390
- trip: fullTrip
391
- },
392
- idemKey: `trip:${fullTrip.trip_id}`
393
- });
301
+ this.insertEvent({ actor, kind: 'trip.logged', subjectId: fullTrip.trip_id, payload: { trip: fullTrip }, idemKey: `trip:${fullTrip.trip_id}` });
394
302
  }
395
- trip = {
396
- appended: gate.appended,
397
- tripId: gate.tripId,
398
- reason: gate.reason
399
- };
303
+ trip = { appended: gate.appended, tripId: gate.tripId, reason: gate.reason };
400
304
  }
401
- return {
402
- recorded,
403
- trip
404
- };
305
+ return { recorded, trip };
405
306
  });
406
307
  return run();
407
308
  }
309
+ // ---- 投影维护 ---------------------------------------------------------------
408
310
  upsertItem(subject, itemId, doc) {
409
311
  const ord = this.db.prepare(`SELECT ord FROM projection_items WHERE subject = ? AND item_id = ? AND tenant_id = ?`).get(subject, itemId, this.tenant)?.ord ?? this.nextOrd(subject);
410
312
  this.db.prepare(`INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)
@@ -417,113 +319,89 @@ export class StateLedger {
417
319
  replaceItems(subject, items) {
418
320
  this.db.prepare('DELETE FROM projection_items WHERE subject = ? AND tenant_id = ?').run(subject, this.tenant);
419
321
  const ins = this.db.prepare('INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)');
420
- items.forEach(([id, doc], i)=>ins.run(this.tenant, subject, id, JSON.stringify(doc), i));
322
+ items.forEach(([id, doc], i) => ins.run(this.tenant, subject, id, JSON.stringify(doc), i));
421
323
  }
422
324
  nextOrd(subject) {
423
325
  return (this.db.prepare('SELECT COALESCE(MAX(ord), -1) AS m FROM projection_items WHERE subject = ? AND tenant_id = ?').get(subject, this.tenant).m ?? -1) + 1;
424
326
  }
327
+ /**
328
+ * fold 重建:DROP 全部投影 → 重放 events(可截到 toSeq)→ 重建投影。
329
+ * 与写路径共用同一套纯函数 → 直读投影与 fold 结果一致(账本/投影永不分叉的可验证性)。
330
+ */
425
331
  rebuildProjections(toSeq) {
426
- const run = this.db.transaction(()=>{
332
+ const run = this.db.transaction(() => {
427
333
  this.db.prepare('DELETE FROM projection_docs WHERE tenant_id = ?').run(this.tenant);
428
334
  this.db.prepare('DELETE FROM projection_items WHERE tenant_id = ?').run(this.tenant);
429
- const rows = (toSeq === undefined ? this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE tenant_id = ? ORDER BY seq') : this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE seq <= ? AND tenant_id = ? ORDER BY seq')).all(...toSeq === undefined ? [
430
- this.tenant
431
- ] : [
432
- toSeq,
433
- this.tenant
434
- ]);
435
- for (const row of rows)this.foldEvent(row);
335
+ const rows = (toSeq === undefined
336
+ ? this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE tenant_id = ? ORDER BY seq')
337
+ : this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE seq <= ? AND tenant_id = ? ORDER BY seq')).all(...(toSeq === undefined ? [this.tenant] : [toSeq, this.tenant]));
338
+ for (const row of rows)
339
+ this.foldEvent(row);
436
340
  const wishes = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ?`).get(this.tenant).n;
437
341
  const companions = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'companions' AND tenant_id = ?`).get(this.tenant).n;
438
- return {
439
- events: rows.length,
440
- wishes,
441
- companions
442
- };
342
+ return { events: rows.length, wishes, companions };
443
343
  });
444
344
  return run();
445
345
  }
446
346
  foldEvent(row) {
447
347
  const p = JSON.parse(row.payload);
448
- switch(row.kind){
449
- case 'motivation.imported':
450
- {
451
- const doc = p['profile'];
452
- this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
348
+ switch (row.kind) {
349
+ case 'motivation.imported': {
350
+ const doc = p['profile'];
351
+ this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
453
352
  ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
454
- break;
455
- }
456
- case 'motivation.patch':
457
- {
458
- const current = this.readMotivation();
459
- const merged = mergeProfile(current, p['patch']);
460
- if (merged) {
461
- const doc = {
462
- ...merged,
463
- updated_at: row.ts
464
- };
465
- this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
353
+ break;
354
+ }
355
+ case 'motivation.patch': {
356
+ const current = this.readMotivation();
357
+ // 重建路径:时间戳 = 事件行 row.ts(账本事实);不在 fold 里再读时钟
358
+ const merged = mergeProfile(current, p['patch'], row.ts);
359
+ if (merged) {
360
+ const doc = { ...merged, updated_at: row.ts };
361
+ this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
466
362
  ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
467
- }
468
- break;
469
363
  }
364
+ break;
365
+ }
470
366
  case 'wish.imported':
471
- case 'wish.added':
472
- {
473
- const wish = p['wish'] ?? p['profile'];
474
- this.insertItem('wish_pool', String(wish.wish_id ?? row.subject_id), wish, row.seq);
475
- break;
476
- }
477
- case 'wish.updated':
478
- {
479
- const id = String(p['wish_id']);
480
- const patch = p['patch'] ?? {};
481
- const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ?`).get(id);
482
- if (rows) {
483
- const doc = {
484
- ...JSON.parse(rows.doc),
485
- ...patch
486
- };
487
- this.upsertItem('wish_pool', id, doc);
488
- }
489
- break;
490
- }
491
- case 'companion.imported':
492
- {
493
- const profile = p['profile'];
494
- this.insertItem('companions', profile.companion_id, profile, row.seq);
495
- break;
496
- }
497
- case 'companion.saved':
498
- {
499
- const patch = p['patch'];
500
- const res = upsertCompanion(this.readCompanions(), patch);
501
- this.replaceItems('companions', res.profiles.map((c)=>[
502
- c.companion_id,
503
- c
504
- ]));
505
- break;
367
+ case 'wish.added': {
368
+ const wish = (p['wish'] ?? p['profile']);
369
+ this.insertItem('wish_pool', String(wish.wish_id ?? row.subject_id), wish, row.seq);
370
+ break;
371
+ }
372
+ case 'wish.updated': {
373
+ const id = String(p['wish_id']);
374
+ const patch = (p['patch'] ?? {});
375
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ? AND tenant_id = ?`).get(id, this.tenant);
376
+ if (rows) {
377
+ const doc = { ...JSON.parse(rows.doc), ...patch };
378
+ this.upsertItem('wish_pool', id, doc);
506
379
  }
507
- default:
508
380
  break;
381
+ }
382
+ case 'companion.imported': {
383
+ const profile = p['profile'];
384
+ this.insertItem('companions', profile.companion_id, profile, row.seq);
385
+ break;
386
+ }
387
+ case 'companion.saved': {
388
+ const patch = p['patch'];
389
+ const res = upsertCompanion(this.readCompanions(), patch);
390
+ this.replaceItems('companions', res.profiles.map(c => [c.companion_id, c]));
391
+ break;
392
+ }
393
+ default:
394
+ break; // 日志类事件(memory_utility/trip/async/write/forget)无投影
509
395
  }
510
396
  }
397
+ // ---- durable 工单(TS-3) -----------------------------------------------------
511
398
  createWorkflowRun(input, actor = 'system:async-request') {
512
399
  const ts = new Date().toISOString();
513
- const run = this.db.transaction(()=>{
400
+ const run = this.db.transaction(() => {
514
401
  this.db.prepare(`INSERT INTO workflow_runs (tenant_id, id, goal, status, ticket_json, state_json, deliverable, created, updated)
515
402
  VALUES (?, ?, ?, 'pending', ?, ?, NULL, ?, ?)
516
403
  ON CONFLICT(tenant_id, id) DO UPDATE SET updated = excluded.updated`).run(this.tenant, input.id, input.goal, JSON.stringify(input.ticket), JSON.stringify(input.state), ts, ts);
517
- this.insertEvent({
518
- actor,
519
- kind: 'async.run_created',
520
- subjectId: input.id,
521
- payload: {
522
- ticket: input.ticket
523
- },
524
- runId: input.id,
525
- ts
526
- });
404
+ this.insertEvent({ actor, kind: 'async.run_created', subjectId: input.id, payload: { ticket: input.ticket }, runId: input.id, ts });
527
405
  });
528
406
  run();
529
407
  }
@@ -544,24 +422,22 @@ export class StateLedger {
544
422
  this.db.prepare(`UPDATE workflow_steps SET status = 'done', result = ?, done_ts = ? WHERE run_id = ? AND name = ? AND tenant_id = ?`).run(JSON.stringify(result), new Date().toISOString(), runId, name, this.tenant);
545
423
  }
546
424
  finishWorkflowRun(id, status, eventKind, deliverable, terminalOutcome, actor = 'system:async-collect') {
547
- const run = this.db.transaction(()=>{
425
+ const run = this.db.transaction(() => {
548
426
  const update = this.db.prepare(`UPDATE workflow_runs SET status = ?, deliverable = ?, updated = ?
549
427
  WHERE id = ? AND tenant_id = ? AND status = 'pending'`).run(status, deliverable, new Date().toISOString(), id, this.tenant);
550
428
  if (update.changes === 0) {
551
429
  const existing = this.getWorkflowRun(id);
552
- if (existing?.status === status) return;
430
+ if (existing?.status === status)
431
+ return;
553
432
  throw new Error(`workflow ${id} cannot transition ${existing?.status ?? 'missing'} -> ${status}`);
554
433
  }
555
434
  this.insertEvent({
556
435
  actor,
557
436
  kind: eventKind,
558
437
  subjectId: id,
559
- payload: {
560
- bytes: deliverable.length,
561
- terminal_outcome: terminalOutcome
562
- },
438
+ payload: { bytes: deliverable.length, terminal_outcome: terminalOutcome },
563
439
  idemKey: `async-terminal:${id}`,
564
- runId: id
440
+ runId: id,
565
441
  });
566
442
  });
567
443
  run();
@@ -576,149 +452,112 @@ export class StateLedger {
576
452
  const row = this.db.prepare(`SELECT payload FROM events
577
453
  WHERE tenant_id = ? AND run_id = ? AND kind IN ('async.settled', 'async.failed')
578
454
  ORDER BY seq DESC LIMIT 1`).get(this.tenant, id);
579
- if (!row) return null;
455
+ if (!row)
456
+ return null;
580
457
  try {
581
458
  const payload = JSON.parse(row.payload);
582
459
  return payload.terminal_outcome ?? null;
583
- } catch {
460
+ }
461
+ catch {
584
462
  return null;
585
463
  }
586
464
  }
465
+ // ---- WriteGate saga 基座(TS-4, RFC S4 L2/L3 物理预备) -----------------------
466
+ /** 登记一笔待确认外部写(幂等键去重:同一确认不可能登记两次)。L2 = 只登记不执行。 */
587
467
  requestPendingWrite(input, actor = 'system:writegate') {
588
468
  const ts = new Date().toISOString();
589
- const run = this.db.transaction(()=>{
469
+ const run = this.db.transaction(() => {
590
470
  const info = this.db.prepare(`INSERT INTO pending_writes (tenant_id, idem_key, seam, payload, status, created, updated)
591
471
  VALUES (?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(tenant_id, idem_key) DO NOTHING`).run(this.tenant, input.idemKey, input.seam, JSON.stringify(input.payload ?? {}), ts, ts);
592
472
  if (info.changes === 0) {
593
473
  const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(input.idemKey, this.tenant);
594
- return {
595
- created: false,
596
- status: row.status
597
- };
474
+ return { created: false, status: row.status };
598
475
  }
599
- this.insertEvent({
600
- actor,
601
- kind: 'write.pending',
602
- subjectId: input.idemKey,
603
- payload: {
604
- seam: input.seam,
605
- payload: input.payload
606
- },
607
- idemKey: `pw:${sha(input.idemKey)}:pending`
608
- });
609
- return {
610
- created: true,
611
- status: 'pending'
612
- };
476
+ this.insertEvent({ actor, kind: 'write.pending', subjectId: input.idemKey, payload: { seam: input.seam, payload: input.payload }, idemKey: `pw:${sha(input.idemKey)}:pending` });
477
+ return { created: true, status: 'pending' };
613
478
  });
614
479
  return run();
615
480
  }
481
+ /** L3:具名 seam 确认——pending → confirmed,携 receipt(外部世界回执) */
616
482
  confirmPendingWrite(idemKey, receipt, actor = 'system:writegate') {
617
- const run = this.db.transaction(()=>{
483
+ const run = this.db.transaction(() => {
618
484
  const info = this.db.prepare(`UPDATE pending_writes SET status = 'confirmed', receipt = ?, updated = ? WHERE idem_key = ? AND status = 'pending' AND tenant_id = ?`).run(receipt, new Date().toISOString(), idemKey, this.tenant);
619
485
  if (info.changes === 0) {
620
486
  const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
621
- return {
622
- ok: false,
623
- status: row?.status ?? 'missing'
624
- };
487
+ return { ok: false, status: row?.status ?? 'missing' };
625
488
  }
626
- this.insertEvent({
627
- actor,
628
- kind: 'write.confirmed',
629
- subjectId: idemKey,
630
- payload: {
631
- receipt
632
- }
633
- });
634
- return {
635
- ok: true,
636
- status: 'confirmed'
637
- };
489
+ this.insertEvent({ actor, kind: 'write.confirmed', subjectId: idemKey, payload: { receipt } });
490
+ return { ok: true, status: 'confirmed' };
638
491
  });
639
492
  return run();
640
493
  }
494
+ /** saga 补偿:pending/confirmed → compensated(SagaLLM 式补偿语义) */
641
495
  compensatePendingWrite(idemKey, note, actor = 'system:writegate') {
642
- const run = this.db.transaction(()=>{
496
+ const run = this.db.transaction(() => {
643
497
  const info = this.db.prepare(`UPDATE pending_writes SET status = 'compensated', receipt = COALESCE(receipt, ?), updated = ? WHERE idem_key = ? AND status != 'compensated' AND tenant_id = ?`).run(note, new Date().toISOString(), idemKey, this.tenant);
644
498
  if (info.changes === 0) {
645
499
  const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
646
- return {
647
- ok: false,
648
- status: row?.status ?? 'missing'
649
- };
500
+ return { ok: false, status: row?.status ?? 'missing' };
650
501
  }
651
- this.insertEvent({
652
- actor,
653
- kind: 'write.compensated',
654
- subjectId: idemKey,
655
- payload: {
656
- note
657
- }
658
- });
659
- return {
660
- ok: true,
661
- status: 'compensated'
662
- };
502
+ this.insertEvent({ actor, kind: 'write.compensated', subjectId: idemKey, payload: { note } });
503
+ return { ok: true, status: 'compensated' };
663
504
  });
664
505
  return run();
665
506
  }
666
507
  listPendingWrites() {
667
508
  return this.db.prepare('SELECT idem_key, seam, status, receipt, created FROM pending_writes WHERE tenant_id = ? ORDER BY created').all(this.tenant);
668
509
  }
510
+ /** what-if 分叉:VACUUM INTO 副本,预演不触正本(LoopX「先只读投影后执行」的物理化) */
669
511
  forkWhatIf(destPath) {
670
512
  this.db.prepare('VACUUM INTO ?').run(destPath);
671
513
  return destPath;
672
514
  }
515
+ // ---- forget(D5:物理硬删,审计只留一行) ---------------------------------------
516
+ /** 物理删除某主体的全部事件并重建投影;删除本身留一行审计事件(红线 6「可删除」)。 */
673
517
  forgetSubject(subjects, actor = 'system:state-cli') {
674
- const run = this.db.transaction(()=>{
518
+ const run = this.db.transaction(() => {
675
519
  let deleted = 0;
676
- for (const s of subjects){
677
- const placeholders = s.kinds.map(()=>'?').join(',');
678
- const info = this.db.prepare(`DELETE FROM events WHERE subject_id = ? AND tenant_id = ? AND kind IN (${placeholders})`).run(s.subjectId, this.tenant, ...s.kinds);
520
+ for (const s of subjects) {
521
+ const placeholders = s.kinds.map(() => '?').join(',');
522
+ const info = this.db.prepare('DELETE FROM events WHERE subject_id = ? AND tenant_id = ? AND kind IN (' + placeholders + ')').run(s.subjectId, this.tenant, ...s.kinds);
679
523
  deleted += info.changes;
680
524
  }
681
- this.insertEvent({
682
- actor,
683
- kind: 'forget.executed',
684
- payload: {
685
- subjects
686
- }
687
- });
525
+ this.insertEvent({ actor, kind: 'forget.executed', payload: { subjects } });
688
526
  this.rebuildProjections();
689
- return {
690
- deleted
691
- };
527
+ return { deleted };
692
528
  });
693
529
  return run();
694
530
  }
695
531
  close() {
696
532
  this.db.close();
697
- for (const [key, value] of openLedgers)if (value === this) openLedgers.delete(key);
533
+ for (const [key, value] of openLedgers)
534
+ if (value === this)
535
+ openLedgers.delete(key);
698
536
  }
699
537
  }
538
+ // ---- 打开/迁移(one-shot D2;模块级缓存避免每次读开新句柄) ----------------------
700
539
  const openLedgers = new Map();
701
540
  export function openDb(stateRoot, tenant = 'local') {
702
541
  const path = ledgerDbPath(stateRoot);
703
542
  const cacheKey = `${path}::${tenant}`;
704
543
  const cached = openLedgers.get(cacheKey);
705
- if (cached) return cached;
706
- mkdirSync(dirname(path), {
707
- recursive: true
708
- });
544
+ if (cached)
545
+ return cached;
546
+ mkdirSync(dirname(path), { recursive: true });
709
547
  const db = new Database(path);
710
548
  db.pragma('journal_mode = WAL');
711
549
  db.pragma('synchronous = NORMAL');
712
550
  db.pragma('busy_timeout = 5000');
713
- const version = db.pragma('user_version', {
714
- simple: true
715
- });
551
+ // schema v1→v2 迁移:v1 无 tenant_id 列(user_version 未设置过,实际为 0)
552
+ const version = db.pragma('user_version', { simple: true });
716
553
  if (version < 2) {
554
+ // 检测是否真有 v1 数据(有 events 表但无 tenant_id 列)
717
555
  const hasEventsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get() !== undefined;
718
- const eventsHasTenant = hasEventsTable && db.prepare("PRAGMA table_info(events)").all().some((c)=>c.name === 'tenant_id');
556
+ const eventsHasTenant = hasEventsTable && db.prepare("PRAGMA table_info(events)").all().some(c => c.name === 'tenant_id');
719
557
  const isV1 = hasEventsTable && !eventsHasTenant;
720
- db.transaction(()=>{
558
+ db.transaction(() => {
721
559
  if (isV1) {
560
+ // v1 → v2:events 加 tenant_id,回填 'local';投影/工单/pending_writes 强制重建(旧表无 tenant_id)
722
561
  db.exec(`
723
562
  ALTER TABLE events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
724
563
  DROP INDEX IF EXISTS events_idem;
@@ -729,8 +568,10 @@ export function openDb(stateRoot, tenant = 'local') {
729
568
  DROP TABLE IF EXISTS workflow_steps;
730
569
  DROP TABLE IF EXISTS pending_writes;
731
570
  `);
732
- } else {
733
- const wrHasTenant = db.prepare("PRAGMA table_info(workflow_runs)").all().some((c)=>c.name === 'tenant_id');
571
+ }
572
+ else {
573
+ // v2 幂等:检查 workflow_runs 是否缺 tenant_id(修一次跑坏的迁移)
574
+ const wrHasTenant = db.prepare("PRAGMA table_info(workflow_runs)").all().some(c => c.name === 'tenant_id');
734
575
  if (!wrHasTenant && db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workflow_runs'").get()) {
735
576
  db.exec(`
736
577
  DROP TABLE IF EXISTS workflow_runs;
@@ -743,139 +584,122 @@ export function openDb(stateRoot, tenant = 'local') {
743
584
  }
744
585
  db.exec(SCHEMA);
745
586
  db.pragma('user_version = 2');
746
- db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', '2');
587
+ db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', SCHEMA_VERSION);
747
588
  })();
589
+ // v1 升级后:旧单用户事件只可安全归入 local;即使调用者打开非 local 租户,也只重建 local 投影。
748
590
  if (isV1) {
749
- const ledger = new StateLedger(db, stateRoot, tenant);
591
+ const ledger = new StateLedger(db, stateRoot, 'local');
750
592
  ledger.rebuildProjections();
751
593
  }
752
- } else {
753
- db.exec(SCHEMA);
594
+ }
595
+ else {
596
+ db.exec(SCHEMA); // 幂等建表(IF NOT EXISTS)
754
597
  db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v').run('schema_version', '2');
755
598
  }
756
599
  const ledger = new StateLedger(db, stateRoot, tenant);
757
600
  openLedgers.set(cacheKey, ledger);
758
601
  return ledger;
759
602
  }
603
+ /** 只读场景:账本不存在返回 null(调用方走文件回退) */
760
604
  export function openLedgerIfExists(stateRoot, tenant = 'local') {
761
- if (!ledgerExists(stateRoot)) return null;
605
+ if (!ledgerExists(stateRoot))
606
+ return null;
762
607
  return openDb(stateRoot, tenant);
763
608
  }
609
+ /**
610
+ * 确保账本可用(写路径必经):
611
+ * - DB 已存在 → 直接用
612
+ * - 无 DB 无旧文件 → 建空账本(fresh root,smoke/测试形态)
613
+ * - 无 DB 有旧文件 → 快照 → 建账本 → 旧数据单事务导入为 events(one-shot;
614
+ * 导入幂等键 + kv 旗标双保险,并发 ensure 不重复导入)
615
+ */
764
616
  export function ensureLedger(stateRoot, tenant = 'local') {
765
- if (ledgerExists(stateRoot)) return openDb(stateRoot, tenant);
617
+ if (ledgerExists(stateRoot))
618
+ return openDb(stateRoot, tenant);
766
619
  const dir = stateDirOf(stateRoot);
767
620
  const hasLegacy = legacyFilesPresent(stateRoot);
768
621
  let backupDir = null;
769
622
  if (hasLegacy) {
770
623
  backupDir = join(dir, 'pre-ledger-backup');
771
- mkdirSync(backupDir, {
772
- recursive: true
773
- });
774
- for (const f of LEGACY_FILES){
775
- if (existsSync(join(dir, f))) copyFileSync(join(dir, f), join(backupDir, f));
624
+ mkdirSync(backupDir, { recursive: true });
625
+ for (const f of LEGACY_FILES) {
626
+ if (existsSync(join(dir, f)))
627
+ copyFileSync(join(dir, f), join(backupDir, f));
776
628
  }
777
629
  }
778
- const ledger = openDb(stateRoot, tenant);
779
- if (hasLegacy) importLegacyInto(ledger, dir);
780
- return ledger;
630
+ if (hasLegacy) {
631
+ // JSON/JSONL 是单用户本地状态,没有可审计租户归属;迁移边界=只归入 local,绝不按调用 tenant 猜测或搬移。
632
+ const localLedger = openDb(stateRoot, 'local');
633
+ importLegacyInto(localLedger, dir);
634
+ return tenant === 'local' ? localLedger : openDb(stateRoot, tenant);
635
+ }
636
+ return openDb(stateRoot, tenant);
781
637
  }
782
638
  function importLegacyInto(ledger, dir) {
783
- const imported = {
784
- motivation: false,
785
- wishes: 0,
786
- utilityEvents: 0,
787
- trips: 0,
788
- companions: 0
789
- };
790
- const run = ledger.db.transaction(()=>{
639
+ const imported = { motivation: false, wishes: 0, utilityEvents: 0, trips: 0, companions: 0 };
640
+ const run = ledger.db.transaction(() => {
791
641
  const flag = ledger.db.prepare(`SELECT v FROM kv WHERE k = 'legacy_imported_v1'`).get();
792
- if (flag) return;
793
- const readJsonSafe = (p)=>{
642
+ if (flag)
643
+ return;
644
+ const readJsonSafe = (p) => {
794
645
  try {
795
646
  return JSON.parse(readFileSync(p, 'utf-8'));
796
- } catch {
647
+ }
648
+ catch {
797
649
  return null;
798
650
  }
799
651
  };
652
+ // 动机画像:整档快照事件(fold 直接落投影)
800
653
  const profile = readJsonSafe(join(dir, 'motivation-profile.json'));
801
654
  if (profile) {
802
- if (ledger.insertEvent({
803
- actor: 'system:migrate',
804
- kind: 'motivation.imported',
805
- subjectId: 'motivation',
806
- payload: {
807
- profile
808
- },
809
- idemKey: 'import:motivation'
810
- }) !== null) imported.motivation = true;
655
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'motivation.imported', subjectId: 'motivation', payload: { profile }, idemKey: 'import:motivation' }) !== null)
656
+ imported.motivation = true;
811
657
  }
658
+ // 愿望池:逐条导入(保留原 wish_id)
812
659
  const pool = readJsonSafe(join(dir, 'wish-pool.json'));
813
660
  if (Array.isArray(pool)) {
814
- for (const w of pool){
661
+ for (const w of pool) {
815
662
  const id = String(w['wish_id'] ?? makeWishId(String(w['name'] ?? '')));
816
- const wish = {
817
- ...w,
818
- wish_id: id
819
- };
820
- if (ledger.insertEvent({
821
- actor: 'system:migrate',
822
- kind: 'wish.imported',
823
- subjectId: id,
824
- payload: {
825
- wish
826
- },
827
- idemKey: `import:wish:${id}`
828
- }) !== null) imported.wishes++;
663
+ const wish = { ...w, wish_id: id };
664
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'wish.imported', subjectId: id, payload: { wish }, idemKey: `import:wish:${id}` }) !== null)
665
+ imported.wishes++;
829
666
  }
830
667
  }
668
+ // 效用流:逐行导入(语义 event_id 即幂等键)
831
669
  try {
832
670
  const lines = readFileSync(join(dir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean);
833
- for (const line of lines){
671
+ for (const line of lines) {
834
672
  try {
835
673
  const ev = JSON.parse(line);
836
- if (ledger.insertEvent({
837
- actor: 'system:migrate',
838
- kind: 'memory_utility.event',
839
- subjectId: ev.wish_id,
840
- payload: {
841
- event: ev
842
- },
843
- idemKey: `mu:${ev.event_id}`
844
- }) !== null) imported.utilityEvents++;
845
- } catch {}
674
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'memory_utility.event', subjectId: ev.wish_id, payload: { event: ev }, idemKey: `mu:${ev.event_id}` }) !== null)
675
+ imported.utilityEvents++;
676
+ }
677
+ catch { /* 脏行跳过:导入永不因单行失败而中断 */ }
846
678
  }
847
- } catch {}
679
+ }
680
+ catch { /* 文件不存在 */ }
681
+ // 时间线:逐行导入(trip_id 幂等)
848
682
  try {
849
683
  const lines = readFileSync(join(dir, 'trips.jsonl'), 'utf-8').split('\n').filter(Boolean);
850
- for (const line of lines){
684
+ for (const line of lines) {
851
685
  try {
852
686
  const trip = JSON.parse(line);
853
- if (ledger.insertEvent({
854
- actor: 'system:migrate',
855
- kind: 'trip.logged',
856
- subjectId: trip.trip_id,
857
- payload: {
858
- trip
859
- },
860
- idemKey: `trip:${trip.trip_id}`
861
- }) !== null) imported.trips++;
862
- } catch {}
687
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'trip.logged', subjectId: trip.trip_id, payload: { trip }, idemKey: `trip:${trip.trip_id}` }) !== null)
688
+ imported.trips++;
689
+ }
690
+ catch { /* 脏行跳过 */ }
863
691
  }
864
- } catch {}
692
+ }
693
+ catch { /* 文件不存在 */ }
694
+ // 同行人:逐条导入(保留 companion_id)
865
695
  const companions = readJsonSafe(join(dir, 'companions.json'));
866
696
  if (Array.isArray(companions)) {
867
- for (const c of companions){
697
+ for (const c of companions) {
868
698
  const id = String(c['companion_id'] ?? '');
869
- if (!id) continue;
870
- if (ledger.insertEvent({
871
- actor: 'system:migrate',
872
- kind: 'companion.imported',
873
- subjectId: id,
874
- payload: {
875
- profile: c
876
- },
877
- idemKey: `import:companion:${id}`
878
- }) !== null) imported.companions++;
699
+ if (!id)
700
+ continue;
701
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'companion.imported', subjectId: id, payload: { profile: c }, idemKey: `import:companion:${id}` }) !== null)
702
+ imported.companions++;
879
703
  }
880
704
  }
881
705
  ledger.rebuildProjections();
@@ -884,45 +708,60 @@ function importLegacyInto(ledger, dir) {
884
708
  run();
885
709
  return imported;
886
710
  }
711
+ // ---- 读路径统一出口:账本优先,未迁移 root 回退旧文件(只读,绝不反向写) ---------
887
712
  function readLegacyJson(stateRoot, name, fallback) {
888
713
  try {
889
714
  return JSON.parse(readFileSync(join(stateDirOf(stateRoot), name), 'utf-8'));
890
- } catch {
715
+ }
716
+ catch {
891
717
  return fallback;
892
718
  }
893
719
  }
894
720
  function readLegacyJsonl(stateRoot, name) {
895
721
  try {
896
- return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
897
- } catch {
722
+ return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
723
+ }
724
+ catch {
898
725
  return [];
899
726
  }
900
727
  }
901
- export function readMotivationWithFallback(stateRoot) {
902
- const ledger = openLedgerIfExists(stateRoot);
903
- if (ledger) return ledger.readMotivation();
728
+ export function readMotivationWithFallback(stateRoot, tenant = 'local') {
729
+ const ledger = openLedgerIfExists(stateRoot, tenant);
730
+ if (ledger)
731
+ return ledger.readMotivation();
732
+ if (tenant !== 'local')
733
+ return null;
904
734
  return readLegacyJson(stateRoot, 'motivation-profile.json', null);
905
735
  }
906
- export function readWishPoolWithFallback(stateRoot) {
907
- const ledger = openLedgerIfExists(stateRoot);
908
- if (ledger) return ledger.readWishPool();
736
+ export function readWishPoolWithFallback(stateRoot, tenant = 'local') {
737
+ const ledger = openLedgerIfExists(stateRoot, tenant);
738
+ if (ledger)
739
+ return ledger.readWishPool();
740
+ if (tenant !== 'local')
741
+ return [];
909
742
  return readLegacyJson(stateRoot, 'wish-pool.json', []);
910
743
  }
911
- export function readUtilityEventsWithFallback(stateRoot) {
912
- const ledger = openLedgerIfExists(stateRoot);
913
- if (ledger) return ledger.readUtilityEvents();
744
+ export function readUtilityEventsWithFallback(stateRoot, tenant = 'local') {
745
+ const ledger = openLedgerIfExists(stateRoot, tenant);
746
+ if (ledger)
747
+ return ledger.readUtilityEvents();
748
+ if (tenant !== 'local')
749
+ return [];
914
750
  return readLegacyJsonl(stateRoot, 'memory-utility.jsonl');
915
751
  }
916
- export function readTripsWithFallback(stateRoot) {
917
- const ledger = openLedgerIfExists(stateRoot);
918
- if (ledger) return ledger.readTrips();
752
+ export function readTripsWithFallback(stateRoot, tenant = 'local') {
753
+ const ledger = openLedgerIfExists(stateRoot, tenant);
754
+ if (ledger)
755
+ return ledger.readTrips();
756
+ if (tenant !== 'local')
757
+ return [];
919
758
  return readLegacyJsonl(stateRoot, 'trips.jsonl');
920
759
  }
921
- export function readCompanionsWithFallback(stateRoot) {
922
- const ledger = openLedgerIfExists(stateRoot);
923
- if (ledger) return ledger.readCompanions();
760
+ export function readCompanionsWithFallback(stateRoot, tenant = 'local') {
761
+ const ledger = openLedgerIfExists(stateRoot, tenant);
762
+ if (ledger)
763
+ return ledger.readCompanions();
764
+ if (tenant !== 'local')
765
+ return [];
924
766
  return readLegacyJson(stateRoot, 'companions.json', []);
925
767
  }
926
-
927
-
928
- //# sourceURL=ts/src/state-ledger.ts