@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27

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