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

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 (533) 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 +1788 -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 +161 -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 +553 -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 +676 -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/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -1,3 +1,33 @@
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
+ *
21
+ * 跨进程争用纪律(D-15 演练 D15-1/D15-2 的修复,issue #275 已于 2026-10-05
22
+ * 作为 deferred 关闭——触发出现时另开新 issue):
23
+ * - busy_timeout 必须在任何可争用语句之前装好(含 journal_mode = WAL);
24
+ * - 建表/迁移整段在 BEGIN IMMEDIATE 里做,版本探测与建表同处一个写锁;
25
+ * - 读改写事务一律 BEGIN IMMEDIATE(先读后写的 DEFERRED 事务升级锁时,
26
+ * SQLite 直接返回 SQLITE_BUSY_SNAPSHOT 且不调用 busy handler);
27
+ * - 争用重试有界,耗尽后抛 LedgerBusyError(典型化),不再漏裸 SQLITE_BUSY*。
28
+ * 单写者产品形态(ADR-16 一个账本一个 owner)本来就不会走到这些竞态,
29
+ * 以上只改争用路径的行为,单进程行为逐字节不变。
30
+ */
1
31
  import Database from 'better-sqlite3';
2
32
  import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
3
33
  import { createHash } from 'node:crypto';
@@ -6,13 +36,15 @@ import { mergeProfile } from './memory-capture.js';
6
36
  import { appendEvent as gateUtilityEvent, projectUtility } from './memory-utility.js';
7
37
  import { appendTrip as gateTrip } from './travel-timeline.js';
8
38
  import { upsertCompanion } from './companions.js';
9
- const SCHEMA_VERSION = '1';
39
+ const SCHEMA_VERSION = '2';
40
+ /** 稳定 wish 主键:名称语义派生(重放/同名幂等更新不再依赖墙钟;RFC §1.1 id 不稳定缺口的修复) */
10
41
  export function makeWishId(name) {
11
42
  return 'w-' + createHash('sha256').update(String(name).trim()).digest('hex').slice(0, 10);
12
43
  }
13
44
  function sha(s) {
14
45
  return createHash('sha256').update(s).digest('hex');
15
46
  }
47
+ /** stateRoot 可相对可绝对(与 incident-log.resolveIncidentsPath 同款解析) */
16
48
  function stateDirOf(stateRoot) {
17
49
  const root = isAbsolute(stateRoot) ? stateRoot : join(process.cwd(), stateRoot);
18
50
  return join(root, 'gotry-state');
@@ -23,23 +55,115 @@ export function ledgerDbPath(stateRoot) {
23
55
  export function ledgerExists(stateRoot) {
24
56
  return existsSync(ledgerDbPath(stateRoot));
25
57
  }
26
- const LEGACY_FILES = [
27
- 'motivation-profile.json',
28
- 'wish-pool.json',
29
- 'memory-utility.jsonl',
30
- 'trips.jsonl',
31
- 'companions.json'
32
- ];
58
+ const LEGACY_FILES = ['motivation-profile.json', 'wish-pool.json', 'memory-utility.jsonl', 'trips.jsonl', 'companions.json'];
33
59
  function legacyFilesPresent(stateRoot) {
34
60
  const dir = stateDirOf(stateRoot);
35
- return LEGACY_FILES.some((f)=>existsSync(join(dir, f)));
61
+ return LEGACY_FILES.some(f => existsSync(join(dir, f)));
36
62
  }
37
63
  function isUniqueViolation(e) {
38
64
  return e instanceof Error && e.code === 'SQLITE_CONSTRAINT_UNIQUE';
39
65
  }
66
+ // ---- 跨进程争用:有界重试 + 典型化出口(D15-1/D15-2) --------------------------
67
+ /** 写锁等待上限;跨进程(dsh 运行时 / async-collect / state-cli tick)共存的容忍面。 */
68
+ const BUSY_TIMEOUT_MS = 5_000;
69
+ /** 争用重试次数上限(首次尝试之外);退避总额 < 400ms,宁可早报错也不无限等。 */
70
+ const CONTENTION_RETRIES = 5;
71
+ /**
72
+ * 打开路径(WAL 切换 / 建表迁移)另按**墙钟**封顶:冷启动竞赛里失败往往是立即
73
+ * 抛出(busy handler 根本没被调用),次数上限会退化成「次数 × busy_timeout」那么长。
74
+ * 一次 open 最多争用 5s —— 正好等于 busy_timeout 的语义,然后给典型化错误。
75
+ */
76
+ const OPEN_CONTENTION_BUDGET_MS = 5_000;
77
+ const OPEN_CONTENTION_RETRIES = 20;
78
+ /**
79
+ * SQLite 把「此刻拿不到锁」编码成这几个码:
80
+ * SQLITE_BUSY / _TIMEOUT 写锁被别的连接占着(busy handler 已耗尽)
81
+ * SQLITE_BUSY_SNAPSHOT 读快照过期、无法升级为写(DEFERRED 读改写的死法)
82
+ * SQLITE_BUSY_RECOVERY WAL 正在被别的连接恢复
83
+ * SQLITE_PROTOCOL WAL index(-shm)争用导致的锁协议重试
84
+ * 只有这几个码允许重试;其余码(约束冲突/IO 错误/逻辑错)一律原样抛出——
85
+ * 真错误不得被重试掩盖成「忙」。
86
+ */
87
+ const CONTENTION_CODES = new Set([
88
+ 'SQLITE_BUSY',
89
+ 'SQLITE_BUSY_TIMEOUT',
90
+ 'SQLITE_BUSY_SNAPSHOT',
91
+ 'SQLITE_BUSY_RECOVERY',
92
+ 'SQLITE_PROTOCOL',
93
+ ]);
94
+ function sqliteCodeOf(e) {
95
+ const code = e?.code;
96
+ return typeof code === 'string' ? code : undefined;
97
+ }
98
+ function isContention(e) {
99
+ const code = sqliteCodeOf(e);
100
+ return code !== undefined && CONTENTION_CODES.has(code);
101
+ }
102
+ /**
103
+ * 典型化的账本争用错误:有界重试耗尽后的唯一出口。
104
+ *
105
+ * 修复前跨进程争用会把 better-sqlite3 的裸 SQLITE_BUSY / SQLITE_BUSY_SNAPSHOT
106
+ * 直接扔给调用方(D15-1/D15-2),调用方只能嗅探字符串码。现在调用方可以
107
+ * `instanceof LedgerBusyError` 判定「只是忙、可稍后再来」,底层码仍保留在
108
+ * `sqliteCode`/`cause` 里供诊断。
109
+ */
110
+ export class LedgerBusyError extends Error {
111
+ /** 与 better-sqlite3 的 `code` 面同形,便于统一嗅探;永不与 SQLITE_* 撞名。 */
112
+ code = 'GOTRY_LEDGER_BUSY';
113
+ op;
114
+ attempts;
115
+ sqliteCode;
116
+ constructor(op, attempts, cause) {
117
+ super(`ledger busy: ${op} 在 ${attempts} 次尝试后仍未取得写锁(末次 SQLite 码 ${sqliteCodeOf(cause) ?? 'unknown'})`, { cause });
118
+ this.name = 'LedgerBusyError';
119
+ this.op = op;
120
+ this.attempts = attempts;
121
+ this.sqliteCode = sqliteCodeOf(cause);
122
+ }
123
+ }
124
+ /** 同步退避:better-sqlite3 全同步,不能 await;Atomics.wait 是仓内既有惯用法。 */
125
+ function sleepSync(ms) {
126
+ if (ms <= 0)
127
+ return;
128
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
129
+ }
130
+ /** 指数退避 + pid 抖动:N 个进程同时冷启动时不会同节奏再撞第二次。 */
131
+ function backoffMs(attempt) {
132
+ return Math.min(5 * 2 ** attempt, 160) + (process.pid % 11);
133
+ }
134
+ /**
135
+ * 有界重试争用码,耗尽即抛 LedgerBusyError。
136
+ *
137
+ * 只对「整体提交或整体回滚」的单元使用:事务函数回滚后守门纯函数会重新读当前
138
+ * 状态再算一遍,所以重跑是幂等的(事件另有 idem_key UNIQUE 兜底)。
139
+ */
140
+ function withContentionRetry(op, fn, opts = {}) {
141
+ const retries = opts.retries ?? CONTENTION_RETRIES;
142
+ const deadline = opts.budgetMs === undefined ? undefined : Date.now() + opts.budgetMs;
143
+ let lastError;
144
+ let attempts = 0;
145
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
146
+ attempts += 1;
147
+ try {
148
+ return fn();
149
+ }
150
+ catch (e) {
151
+ if (!isContention(e))
152
+ throw e;
153
+ lastError = e;
154
+ if (attempt >= retries)
155
+ break;
156
+ if (deadline !== undefined && Date.now() >= deadline)
157
+ break;
158
+ sleepSync(backoffMs(attempt));
159
+ }
160
+ }
161
+ throw new LedgerBusyError(op, attempts, lastError);
162
+ }
40
163
  const SCHEMA = `
41
164
  CREATE TABLE IF NOT EXISTS events (
42
165
  seq INTEGER PRIMARY KEY AUTOINCREMENT,
166
+ tenant_id TEXT NOT NULL DEFAULT 'local',
43
167
  ts TEXT NOT NULL,
44
168
  actor TEXT NOT NULL,
45
169
  kind TEXT NOT NULL,
@@ -48,48 +172,56 @@ CREATE TABLE IF NOT EXISTS events (
48
172
  idem_key TEXT,
49
173
  run_id TEXT
50
174
  );
51
- CREATE UNIQUE INDEX IF NOT EXISTS events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;
175
+ CREATE UNIQUE INDEX IF NOT EXISTS events_idem ON events(tenant_id, idem_key) WHERE idem_key IS NOT NULL;
52
176
 
53
177
  CREATE TABLE IF NOT EXISTS projection_docs (
54
- subject TEXT PRIMARY KEY,
55
- doc TEXT NOT NULL
178
+ tenant_id TEXT NOT NULL DEFAULT 'local',
179
+ subject TEXT NOT NULL,
180
+ doc TEXT NOT NULL,
181
+ PRIMARY KEY (tenant_id, subject)
56
182
  );
57
183
  CREATE TABLE IF NOT EXISTS projection_items (
184
+ tenant_id TEXT NOT NULL DEFAULT 'local',
58
185
  subject TEXT NOT NULL,
59
186
  item_id TEXT NOT NULL,
60
187
  doc TEXT NOT NULL,
61
188
  ord INTEGER NOT NULL DEFAULT 0,
62
- PRIMARY KEY (subject, item_id)
189
+ PRIMARY KEY (tenant_id, subject, item_id)
63
190
  );
64
191
 
65
192
  CREATE TABLE IF NOT EXISTS workflow_runs (
66
- id TEXT PRIMARY KEY,
193
+ tenant_id TEXT NOT NULL DEFAULT 'local',
194
+ id TEXT NOT NULL,
67
195
  goal TEXT NOT NULL,
68
196
  status TEXT NOT NULL CHECK (status IN ('pending','settled','failed')),
69
197
  ticket_json TEXT NOT NULL,
70
198
  state_json TEXT NOT NULL,
71
199
  deliverable TEXT,
72
200
  created TEXT NOT NULL,
73
- updated TEXT NOT NULL
201
+ updated TEXT NOT NULL,
202
+ PRIMARY KEY (tenant_id, id)
74
203
  );
75
204
  CREATE TABLE IF NOT EXISTS workflow_steps (
205
+ tenant_id TEXT NOT NULL DEFAULT 'local',
76
206
  run_id TEXT NOT NULL,
77
207
  name TEXT NOT NULL,
78
208
  status TEXT NOT NULL CHECK (status IN ('intent','done','failed')),
79
209
  result TEXT,
80
210
  intent_ts TEXT NOT NULL,
81
211
  done_ts TEXT,
82
- PRIMARY KEY (run_id, name)
212
+ PRIMARY KEY (tenant_id, run_id, name)
83
213
  );
84
214
 
85
215
  CREATE TABLE IF NOT EXISTS pending_writes (
86
- idem_key TEXT PRIMARY KEY,
216
+ tenant_id TEXT NOT NULL DEFAULT 'local',
217
+ idem_key TEXT NOT NULL,
87
218
  seam TEXT NOT NULL,
88
219
  payload TEXT NOT NULL,
89
220
  status TEXT NOT NULL CHECK (status IN ('pending','confirmed','compensated')),
90
221
  receipt TEXT,
91
222
  created TEXT NOT NULL,
92
- updated TEXT NOT NULL
223
+ updated TEXT NOT NULL,
224
+ PRIMARY KEY (tenant_id, idem_key)
93
225
  );
94
226
 
95
227
  CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);
@@ -97,101 +229,131 @@ CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);
97
229
  export class StateLedger {
98
230
  db;
99
231
  stateRoot;
232
+ tenant;
100
233
  dbPath;
101
- constructor(db, stateRoot){
234
+ constructor(db, stateRoot, tenant = 'local') {
102
235
  this.db = db;
103
236
  this.stateRoot = stateRoot;
237
+ this.tenant = tenant;
104
238
  this.dbPath = ledgerDbPath(stateRoot);
105
239
  }
240
+ // ---- 基础事件面 ------------------------------------------------------------
241
+ /** 事件插入(idem 命中 → 返回 null,不抛;红线校验异常向上抛 = 事务回滚) */
106
242
  insertEvent(ev) {
107
- const stmt = this.db.prepare(`INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id)
108
- VALUES (?, ?, ?, ?, ?, ?, ?)`);
243
+ const stmt = this.db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
244
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
109
245
  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);
246
+ 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
247
  return Number(info.lastInsertRowid);
112
- } catch (e) {
113
- if (isUniqueViolation(e)) return null;
248
+ }
249
+ catch (e) {
250
+ if (isUniqueViolation(e))
251
+ return null;
114
252
  throw e;
115
253
  }
116
254
  }
117
255
  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
- ]);
256
+ const rows = kind
257
+ ? this.db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
258
+ FROM events WHERE tenant_id = ? AND kind = ? ORDER BY seq DESC LIMIT ?`).all(this.tenant, kind, limit)
259
+ : this.db.prepare(`SELECT seq, tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id
260
+ FROM events WHERE tenant_id = ? ORDER BY seq DESC LIMIT ?`).all(this.tenant, limit);
126
261
  return rows;
127
262
  }
128
263
  countEvents() {
129
- return this.db.prepare('SELECT COUNT(*) AS n FROM events').get().n;
264
+ return this.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id = ?').get(this.tenant).n;
130
265
  }
266
+ // ---- 读:投影与日志类事件 ----------------------------------------------------
131
267
  readMotivation() {
132
- const row = this.db.prepare(`SELECT doc FROM projection_docs WHERE subject = 'motivation'`).get();
268
+ const row = this.db.prepare(`SELECT doc FROM projection_docs WHERE subject = 'motivation' AND tenant_id = ?`).get(this.tenant);
133
269
  return row ? JSON.parse(row.doc) : null;
134
270
  }
135
271
  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));
272
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ? ORDER BY ord, rowid`).all(this.tenant);
273
+ return rows.map(r => JSON.parse(r.doc));
138
274
  }
139
275
  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));
276
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'companions' AND tenant_id = ? ORDER BY ord, rowid`).all(this.tenant);
277
+ return rows.map(r => JSON.parse(r.doc));
142
278
  }
143
279
  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);
280
+ const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'memory_utility.event' AND tenant_id = ? ORDER BY seq`).all(this.tenant);
281
+ return rows.map(r => JSON.parse(r.payload).event);
146
282
  }
147
283
  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);
284
+ const rows = this.db.prepare(`SELECT payload FROM events WHERE kind = 'trip.logged' AND tenant_id = ? ORDER BY seq`).all(this.tenant);
285
+ return rows.map(r => JSON.parse(r.payload).trip);
150
286
  }
287
+ /** 效用投影(与文件版 projectUtility 同源同构) */
151
288
  projectUtilityNow() {
152
289
  return projectUtility(this.readUtilityEvents());
153
290
  }
291
+ // ---- 写:单事务{INSERT event; UPDATE 投影},守门纯函数原样复用 -----------------
292
+ // 红线进事务:evidence 缺失/conditions 空 → mergeProfile/校验拒绝 → 事务回滚,账本无痕。
293
+ /**
294
+ * 读改写事务的统一出口(D15-2 修复)。
295
+ *
296
+ * 为什么必须 BEGIN IMMEDIATE:DEFERRED 事务的第一条语句是 SELECT 时,SQLite 只
297
+ * 取读快照;等后面要写时若已有别的写者提交过,快照无法升级,SQLite **立即**返回
298
+ * SQLITE_BUSY_SNAPSHOT 并且**不调用 busy handler**(等待毫无意义,快照已过期)。
299
+ * 所以 busy_timeout 对这条路径完全无效——演练里 appendWish 需要调用方自己重试
300
+ * 6~9 次才做完 12 次尝试,就是这个原因。IMMEDIATE 开头就拿写锁,后来者改为在
301
+ * busy handler 里排队等待,SQLITE_BUSY_SNAPSHOT 在物理上不再可能出现。
302
+ *
303
+ * 重试是 IMMEDIATE 之外的第二道兜底(busy_timeout 真被耗尽时),整体回滚后重算,
304
+ * 幂等;耗尽 → LedgerBusyError。已在外层事务里(嵌套 = SAVEPOINT,模式被
305
+ * better-sqlite3 忽略)时不自行重试:重试归最外层事务所有,否则会在一个已经
306
+ * 注定回滚的事务里空转。
307
+ */
308
+ runWrite(op, fn) {
309
+ const run = this.db.transaction(fn);
310
+ if (this.db.inTransaction)
311
+ return run.immediate();
312
+ return withContentionRetry(`ledger.${op}`, () => run.immediate());
313
+ }
314
+ /**
315
+ * 全量事务审计(D15-2):判据只有一条——**事务的第一条语句是不是 SELECT**。
316
+ * 是 → 先读后写,会撞 SQLITE_BUSY_SNAPSHOT,必须走 runWrite(IMMEDIATE+有界重试);
317
+ * 不是(INSERT/UPDATE/DELETE 开头)→ 写锁在第一条语句就拿到,快照升级在物理上
318
+ * 不会发生,维持 DEFERRED 逐字节同义,不扩大改动面。
319
+ *
320
+ * runWrite(IMMEDIATE) appendMotivationPatch readMotivation 开头
321
+ * appendWish readWishPool 开头(演练实测失败点)
322
+ * appendTripEvent readTrips 开头
323
+ * appendCompanion readCompanions 开头
324
+ * confirmOutcome full 为空时以 readTrips 开头
325
+ * importLegacyInto SELECT kv.legacy_imported_v1 开头
326
+ * 维持 DEFERRED appendUtilityEvent 单条 INSERT
327
+ * rebuildProjections DELETE 投影开头(读在写锁之内)
328
+ * createWorkflowRun INSERT workflow_runs 开头
329
+ * finishWorkflowRun UPDATE workflow_runs 开头
330
+ * requestPendingWrite INSERT pending_writes 开头
331
+ * confirmPendingWrite UPDATE pending_writes 开头
332
+ * compensatePendingWrite UPDATE pending_writes 开头
333
+ * forgetSubject DELETE events 开头
334
+ * openDb 的建表/迁移事务另见 ensureSchema(D15-1)。
335
+ */
336
+ /** 动机画像增量补丁(守门 = mergeProfile:追加不删史/幂等/权重变更须伴新证据) */
154
337
  appendMotivationPatch(patch, actor = 'tool:gotry_motivation_save') {
338
+ // 事件边界 ISO:事件行的 ts 就是 homeCityPreference.updated_at 的源头,
339
+ // fold 路径会读同一字段回放——rebuildProjections 必须能复现该值。
155
340
  const ts = new Date().toISOString();
156
- const run = this.db.transaction(()=>{
341
+ // 读改写(readMotivation → mergeProfile → 写投影):第一条语句是 SELECT → IMMEDIATE。
342
+ return this.runWrite('appendMotivationPatch', () => {
157
343
  const current = this.readMotivation();
158
- const merged = mergeProfile(current, patch);
344
+ const merged = mergeProfile(current, patch, ts);
159
345
  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
- };
346
+ const unchanged = { ...(current ?? { weights: {}, evidence: [], hard: {} }), updated_at: current?.updated_at ?? ts };
347
+ return { saved: false, profile: unchanged };
172
348
  }
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
- };
349
+ const doc = { ...merged, updated_at: ts };
350
+ this.insertEvent({ actor, kind: 'motivation.patch', subjectId: 'motivation', payload: { patch }, ts });
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
+ return { saved: true, profile: doc };
192
354
  });
193
- return run();
194
355
  }
356
+ /** 愿望入池/同名幂等更新(红线:conditions 必填非空) */
195
357
  appendWish(entry, actor = 'tool:gotry_wish_pool_add') {
196
358
  const name = String(entry.name ?? '').trim();
197
359
  const conditions = entry.conditions;
@@ -199,9 +361,11 @@ export class StateLedger {
199
361
  throw new Error('wish pool entry requires name and conditions (fulfilment conditions are the whole point)');
200
362
  }
201
363
  const ts = new Date().toISOString();
202
- const run = this.db.transaction(()=>{
364
+ // 读改写(readWishPool → 同名判定 → 写事件+投影):第一条语句是 SELECT → IMMEDIATE。
365
+ // 这是 D15-2 演练里实际失败的那条路径。
366
+ return this.runWrite('appendWish', () => {
203
367
  const pool = this.readWishPool();
204
- const existing = pool.find((w)=>String(w.name ?? '') === name);
368
+ const existing = pool.find(w => String(w.name ?? '') === name);
205
369
  if (existing) {
206
370
  const id = typeof existing.wish_id === 'string' && existing.wish_id ? existing.wish_id : makeWishId(name);
207
371
  const next = {
@@ -209,32 +373,11 @@ export class StateLedger {
209
373
  wish_id: id,
210
374
  reason: entry.reason !== undefined ? entry.reason : existing.reason,
211
375
  conditions,
212
- ...entry.muted !== undefined ? {
213
- muted: Boolean(entry.muted)
214
- } : {}
376
+ ...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}),
215
377
  };
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
- });
378
+ 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
379
  this.upsertItem('wish_pool', id, next);
233
- return {
234
- added: false,
235
- wish_id: id,
236
- total: pool.length
237
- };
380
+ return { added: false, wish_id: id, total: pool.length };
238
381
  }
239
382
  const id = makeWishId(name);
240
383
  const doc = {
@@ -242,607 +385,636 @@ export class StateLedger {
242
385
  name,
243
386
  reason: entry.reason ?? '',
244
387
  conditions,
245
- ...entry.muted !== undefined ? {
246
- muted: Boolean(entry.muted)
247
- } : {},
248
- added_at: ts
388
+ ...(entry.muted !== undefined ? { muted: Boolean(entry.muted) } : {}),
389
+ added_at: ts,
249
390
  };
250
- const seq = this.insertEvent({
251
- actor,
252
- kind: 'wish.added',
253
- subjectId: id,
254
- payload: {
255
- wish: doc
256
- },
257
- ts
258
- });
391
+ const seq = this.insertEvent({ actor, kind: 'wish.added', subjectId: id, payload: { wish: doc }, ts });
259
392
  this.insertItem('wish_pool', id, doc, seq ?? 0);
260
- return {
261
- added: true,
262
- wish_id: id,
263
- total: pool.length + 1
264
- };
393
+ return { added: true, wish_id: id, total: pool.length + 1 };
265
394
  });
266
- return run();
267
395
  }
396
+ /** 效用事件(幂等键 = 语义 event_id;verified 无归因自动降级 applied——纯函数同款纪律) */
268
397
  appendUtilityEvent(ev, actor = 'tool:gotry_wish_pool_list') {
269
398
  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;
399
+ if (!full)
400
+ return { appended: false, events: this.readUtilityEvents() };
401
+ // 非读改写:事务内只有一条 INSERT,写锁在第一条语句就拿到,不存在快照升级。
402
+ // 维持 DEFERRED(与修复前逐字节同义),争用只会落到 busy_timeout 等待上。
403
+ const run = this.db.transaction(() => {
404
+ return this.insertEvent({ actor, kind: 'memory_utility.event', subjectId: full.wish_id, payload: { event: full }, idemKey: `mu:${full.event_id}` }) !== null;
284
405
  });
285
- return {
286
- appended: run(),
287
- events: this.readUtilityEvents()
288
- };
406
+ return { appended: run(), events: this.readUtilityEvents() };
289
407
  }
408
+ /** 时间线行程(守门 = appendTrip:必填/绝对日期/重叠冲突即停;幂等键 = trip_id) */
290
409
  appendTripEvent(ev, actor = 'tool:gotry_trip_log') {
291
- const run = this.db.transaction(()=>{
410
+ // 读改写(readTrips → 重叠冲突判定 → 写事件):第一条语句是 SELECT → IMMEDIATE。
411
+ // 重叠判定必须看见提交后的全量行程,否则两个进程可以各写一条互相重叠的行程。
412
+ return this.runWrite('appendTripEvent', () => {
292
413
  const existing = this.readTrips();
293
414
  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
- };
415
+ if (!gate.appended)
416
+ return { appended: false, tripId: gate.tripId, reason: gate.reason, total: existing.length };
300
417
  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
- };
418
+ this.insertEvent({ actor, kind: 'trip.logged', subjectId: full.trip_id, payload: { trip: full }, idemKey: `trip:${full.trip_id}` });
419
+ return { appended: true, tripId: full.trip_id, total: gate.events.length };
315
420
  });
316
- return run();
317
421
  }
422
+ /** 同行人档案(守门 = upsertCompanion:负面清单拒收/追加不删史/幂等) */
318
423
  appendCompanion(patch, actor = 'tool:gotry_companion_save') {
319
- const run = this.db.transaction(()=>{
424
+ // 读改写(readCompanions → upsertCompanion → DELETE+INSERT 整组投影):
425
+ // 第一条语句是 SELECT → IMMEDIATE。replaceItems 整组重写,丢更新的代价最大。
426
+ return this.runWrite('appendCompanion', () => {
320
427
  const profiles = this.readCompanions();
321
428
  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
- };
429
+ if (!res.appended)
430
+ return { appended: false, companionId: res.companionId, reason: res.reason, total: profiles.length };
431
+ this.insertEvent({ actor, kind: 'companion.saved', subjectId: res.companionId, payload: { patch } });
432
+ this.replaceItems('companions', res.profiles.map(p => [p.companion_id, p]));
433
+ return { appended: true, companionId: res.companionId, total: res.profiles.length };
345
434
  });
346
- return run();
347
435
  }
436
+ /**
437
+ * 成行确认(原跨两文件写 → 单事务):verified_outcome 效用事件 + 可选时间线行程,
438
+ * 同事务提交,崩溃后要么全有要么全无(RFC §1.1 partial-writes 缺口的修复)。
439
+ */
348
440
  confirmOutcome(input, actor = 'tool:gotry_wish_pool_list') {
349
- const run = this.db.transaction(()=>{
441
+ // 读改写:行程分支里 readTrips → gateTrip → 写事件;且 full 为空时整个事务
442
+ // 以 SELECT 开头。两个写同一事务提交的原子性正是本方法的存在理由 → IMMEDIATE。
443
+ return this.runWrite('confirmOutcome', () => {
350
444
  const now = new Date().toISOString();
351
445
  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
446
+ wish_id: input.wishId, kind: 'verified_outcome', ts: now,
447
+ ctx: 'gotry_wish_pool_list.confirm', detail: input.detail, attribution: input.attribution,
358
448
  }).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;
449
+ const recorded = full
450
+ ? this.insertEvent({ actor, kind: 'memory_utility.event', subjectId: full.wish_id, payload: { event: full }, idemKey: `mu:${full.event_id}` }) !== null
451
+ : false;
368
452
  let trip;
369
453
  if (input.trip) {
370
454
  const existing = this.readTrips();
371
455
  const gate = gateTrip(existing, input.trip);
372
456
  if (gate.appended) {
373
457
  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
- });
458
+ this.insertEvent({ actor, kind: 'trip.logged', subjectId: fullTrip.trip_id, payload: { trip: fullTrip }, idemKey: `trip:${fullTrip.trip_id}` });
383
459
  }
384
- trip = {
385
- appended: gate.appended,
386
- tripId: gate.tripId,
387
- reason: gate.reason
388
- };
460
+ trip = { appended: gate.appended, tripId: gate.tripId, reason: gate.reason };
389
461
  }
390
- return {
391
- recorded,
392
- trip
393
- };
462
+ return { recorded, trip };
394
463
  });
395
- return run();
396
464
  }
465
+ // ---- 投影维护 ---------------------------------------------------------------
397
466
  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);
467
+ 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);
468
+ this.db.prepare(`INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)
469
+ ON CONFLICT(tenant_id, subject, item_id) DO UPDATE SET doc = excluded.doc`).run(this.tenant, subject, itemId, JSON.stringify(doc), ord);
401
470
  }
402
471
  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);
472
+ this.db.prepare(`INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)
473
+ ON CONFLICT(tenant_id, subject, item_id) DO UPDATE SET doc = excluded.doc`).run(this.tenant, subject, itemId, JSON.stringify(doc), ord);
405
474
  }
406
475
  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));
476
+ this.db.prepare('DELETE FROM projection_items WHERE subject = ? AND tenant_id = ?').run(subject, this.tenant);
477
+ const ins = this.db.prepare('INSERT INTO projection_items (tenant_id, subject, item_id, doc, ord) VALUES (?, ?, ?, ?, ?)');
478
+ items.forEach(([id, doc], i) => ins.run(this.tenant, subject, id, JSON.stringify(doc), i));
410
479
  }
411
480
  nextOrd(subject) {
412
- return (this.db.prepare('SELECT COALESCE(MAX(ord), -1) AS m FROM projection_items WHERE subject = ?').get(subject).m ?? -1) + 1;
481
+ 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
482
  }
483
+ /**
484
+ * fold 重建:DROP 全部投影 → 重放 events(可截到 toSeq)→ 重建投影。
485
+ * 与写路径共用同一套纯函数 → 直读投影与 fold 结果一致(账本/投影永不分叉的可验证性)。
486
+ */
414
487
  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
- };
488
+ // DELETE 开头 → 写锁即刻到手,维持 DEFERRED(审计表见 runWrite 上方);
489
+ // 且本方法常被 forgetSubject/importLegacyInto 嵌套调用,嵌套层不该自己重试。
490
+ const run = this.db.transaction(() => {
491
+ this.db.prepare('DELETE FROM projection_docs WHERE tenant_id = ?').run(this.tenant);
492
+ this.db.prepare('DELETE FROM projection_items WHERE tenant_id = ?').run(this.tenant);
493
+ const rows = (toSeq === undefined
494
+ ? this.db.prepare('SELECT seq, ts, kind, subject_id, payload FROM events WHERE tenant_id = ? ORDER BY seq')
495
+ : 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]));
496
+ for (const row of rows)
497
+ this.foldEvent(row);
498
+ const wishes = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'wish_pool' AND tenant_id = ?`).get(this.tenant).n;
499
+ const companions = this.db.prepare(`SELECT COUNT(*) AS n FROM projection_items WHERE subject = 'companions' AND tenant_id = ?`).get(this.tenant).n;
500
+ return { events: rows.length, wishes, companions };
429
501
  });
430
502
  return run();
431
503
  }
432
504
  foldEvent(row) {
433
505
  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;
506
+ switch (row.kind) {
507
+ case 'motivation.imported': {
508
+ const doc = p['profile'];
509
+ this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
510
+ ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
511
+ break;
512
+ }
513
+ case 'motivation.patch': {
514
+ const current = this.readMotivation();
515
+ // 重建路径:时间戳 = 事件行 row.ts(账本事实);不在 fold 里再读时钟
516
+ const merged = mergeProfile(current, p['patch'], row.ts);
517
+ if (merged) {
518
+ const doc = { ...merged, updated_at: row.ts };
519
+ this.db.prepare(`INSERT INTO projection_docs (tenant_id, subject, doc) VALUES (?, 'motivation', ?)
520
+ ON CONFLICT(tenant_id, subject) DO UPDATE SET doc = excluded.doc`).run(this.tenant, JSON.stringify(doc));
455
521
  }
522
+ break;
523
+ }
456
524
  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;
525
+ case 'wish.added': {
526
+ const wish = (p['wish'] ?? p['profile']);
527
+ this.insertItem('wish_pool', String(wish.wish_id ?? row.subject_id), wish, row.seq);
528
+ break;
529
+ }
530
+ case 'wish.updated': {
531
+ const id = String(p['wish_id']);
532
+ const patch = (p['patch'] ?? {});
533
+ const rows = this.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ? AND tenant_id = ?`).get(id, this.tenant);
534
+ if (rows) {
535
+ const doc = { ...JSON.parse(rows.doc), ...patch };
536
+ this.upsertItem('wish_pool', id, doc);
492
537
  }
493
- default:
494
538
  break;
539
+ }
540
+ case 'companion.imported': {
541
+ const profile = p['profile'];
542
+ this.insertItem('companions', profile.companion_id, profile, row.seq);
543
+ break;
544
+ }
545
+ case 'companion.saved': {
546
+ const patch = p['patch'];
547
+ const res = upsertCompanion(this.readCompanions(), patch);
548
+ this.replaceItems('companions', res.profiles.map(c => [c.companion_id, c]));
549
+ break;
550
+ }
551
+ default:
552
+ break; // 日志类事件(memory_utility/trip/async/write/forget)无投影
495
553
  }
496
554
  }
555
+ // ---- durable 工单(TS-3) -----------------------------------------------------
497
556
  createWorkflowRun(input, actor = 'system:async-request') {
498
557
  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
- });
558
+ // INSERT 开头 → 维持 DEFERRED(审计表见 runWrite 上方)。
559
+ const run = this.db.transaction(() => {
560
+ this.db.prepare(`INSERT INTO workflow_runs (tenant_id, id, goal, status, ticket_json, state_json, deliverable, created, updated)
561
+ VALUES (?, ?, ?, 'pending', ?, ?, NULL, ?, ?)
562
+ 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);
563
+ this.insertEvent({ actor, kind: 'async.run_created', subjectId: input.id, payload: { ticket: input.ticket }, runId: input.id, ts });
513
564
  });
514
565
  run();
515
566
  }
516
567
  getWorkflowRun(id) {
517
- return this.db.prepare('SELECT * FROM workflow_runs WHERE id = ?').get(id);
568
+ return this.db.prepare('SELECT * FROM workflow_runs WHERE id = ? AND tenant_id = ?').get(id, this.tenant);
518
569
  }
519
570
  pendingWorkflowRuns() {
520
- return this.db.prepare(`SELECT id, goal FROM workflow_runs WHERE status = 'pending' ORDER BY created`).all();
571
+ return this.db.prepare(`SELECT id, goal FROM workflow_runs WHERE status = 'pending' AND tenant_id = ? ORDER BY created`).all(this.tenant);
521
572
  }
522
573
  getWorkflowStep(runId, name) {
523
- return this.db.prepare('SELECT status, result FROM workflow_steps WHERE run_id = ? AND name = ?').get(runId, name);
574
+ return this.db.prepare('SELECT status, result FROM workflow_steps WHERE run_id = ? AND name = ? AND tenant_id = ?').get(runId, name, this.tenant);
524
575
  }
525
576
  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());
577
+ this.db.prepare(`INSERT INTO workflow_steps (tenant_id, run_id, name, status, intent_ts) VALUES (?, ?, ?, 'intent', ?)
578
+ ON CONFLICT(tenant_id, run_id, name) DO NOTHING`).run(this.tenant, runId, name, new Date().toISOString());
528
579
  }
529
580
  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);
581
+ 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);
582
+ }
583
+ finishWorkflowRun(id, status, eventKind, deliverable, terminalOutcome, actor = 'system:async-collect') {
584
+ // UPDATE ... WHERE status = 'pending' 开头(CAS 式状态推进)→ 维持 DEFERRED。
585
+ const run = this.db.transaction(() => {
586
+ const update = this.db.prepare(`UPDATE workflow_runs SET status = ?, deliverable = ?, updated = ?
587
+ WHERE id = ? AND tenant_id = ? AND status = 'pending'`).run(status, deliverable, new Date().toISOString(), id, this.tenant);
588
+ if (update.changes === 0) {
589
+ const existing = this.getWorkflowRun(id);
590
+ if (existing?.status === status)
591
+ return;
592
+ throw new Error(`workflow ${id} cannot transition ${existing?.status ?? 'missing'} -> ${status}`);
593
+ }
535
594
  this.insertEvent({
536
595
  actor,
537
- kind: 'async.settled',
596
+ kind: eventKind,
538
597
  subjectId: id,
539
- payload: {
540
- bytes: deliverable.length
541
- },
542
- runId: id
598
+ payload: { bytes: deliverable.length, terminal_outcome: terminalOutcome },
599
+ idemKey: `async-terminal:${id}`,
600
+ runId: id,
543
601
  });
544
602
  });
545
603
  run();
546
604
  }
605
+ settleWorkflowRun(id, deliverable, terminalOutcome, actor = 'system:async-collect') {
606
+ this.finishWorkflowRun(id, 'settled', 'async.settled', deliverable, terminalOutcome, actor);
607
+ }
608
+ failWorkflowRun(id, deliverable, terminalOutcome, actor = 'system:async-collect') {
609
+ this.finishWorkflowRun(id, 'failed', 'async.failed', deliverable, terminalOutcome, actor);
610
+ }
611
+ getWorkflowTerminalOutcome(id) {
612
+ const row = this.db.prepare(`SELECT payload FROM events
613
+ WHERE tenant_id = ? AND run_id = ? AND kind IN ('async.settled', 'async.failed')
614
+ ORDER BY seq DESC LIMIT 1`).get(this.tenant, id);
615
+ if (!row)
616
+ return null;
617
+ try {
618
+ const payload = JSON.parse(row.payload);
619
+ return payload.terminal_outcome ?? null;
620
+ }
621
+ catch {
622
+ return null;
623
+ }
624
+ }
625
+ // ---- WriteGate saga 基座(TS-4, RFC S4 L2/L3 物理预备) -----------------------
626
+ /** 登记一笔待确认外部写(幂等键去重:同一确认不可能登记两次)。L2 = 只登记不执行。 */
547
627
  requestPendingWrite(input, actor = 'system:writegate') {
548
628
  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);
629
+ // INSERT ... ON CONFLICT DO NOTHING 开头 → 维持 DEFERRED。
630
+ const run = this.db.transaction(() => {
631
+ const info = this.db.prepare(`INSERT INTO pending_writes (tenant_id, idem_key, seam, payload, status, created, updated)
632
+ VALUES (?, ?, ?, ?, 'pending', ?, ?) ON CONFLICT(tenant_id, idem_key) DO NOTHING`).run(this.tenant, input.idemKey, input.seam, JSON.stringify(input.payload ?? {}), ts, ts);
552
633
  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
- };
634
+ const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(input.idemKey, this.tenant);
635
+ return { created: false, status: row.status };
558
636
  }
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
- };
637
+ this.insertEvent({ actor, kind: 'write.pending', subjectId: input.idemKey, payload: { seam: input.seam, payload: input.payload }, idemKey: `pw:${sha(input.idemKey)}:pending` });
638
+ return { created: true, status: 'pending' };
573
639
  });
574
640
  return run();
575
641
  }
642
+ /** L3:具名 seam 确认——pending → confirmed,携 receipt(外部世界回执) */
576
643
  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);
644
+ // UPDATE ... WHERE status = 'pending' 开头(CAS)→ 维持 DEFERRED。
645
+ const run = this.db.transaction(() => {
646
+ 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
647
  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
- };
648
+ const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
649
+ return { ok: false, status: row?.status ?? 'missing' };
585
650
  }
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
- };
651
+ this.insertEvent({ actor, kind: 'write.confirmed', subjectId: idemKey, payload: { receipt } });
652
+ return { ok: true, status: 'confirmed' };
598
653
  });
599
654
  return run();
600
655
  }
656
+ /** saga 补偿:pending/confirmed → compensated(SagaLLM 式补偿语义) */
601
657
  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);
658
+ // UPDATE ... WHERE status != 'compensated' 开头(CAS)→ 维持 DEFERRED。
659
+ const run = this.db.transaction(() => {
660
+ 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
661
  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
- };
662
+ const row = this.db.prepare('SELECT status FROM pending_writes WHERE idem_key = ? AND tenant_id = ?').get(idemKey, this.tenant);
663
+ return { ok: false, status: row?.status ?? 'missing' };
610
664
  }
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
- };
665
+ this.insertEvent({ actor, kind: 'write.compensated', subjectId: idemKey, payload: { note } });
666
+ return { ok: true, status: 'compensated' };
623
667
  });
624
668
  return run();
625
669
  }
626
670
  listPendingWrites() {
627
- return this.db.prepare('SELECT idem_key, seam, status, receipt, created FROM pending_writes ORDER BY created').all();
671
+ return this.db.prepare('SELECT idem_key, seam, status, receipt, created FROM pending_writes WHERE tenant_id = ? ORDER BY created').all(this.tenant);
628
672
  }
673
+ /** what-if 分叉:VACUUM INTO 副本,预演不触正本(LoopX「先只读投影后执行」的物理化) */
629
674
  forkWhatIf(destPath) {
630
675
  this.db.prepare('VACUUM INTO ?').run(destPath);
631
676
  return destPath;
632
677
  }
678
+ // ---- forget(D5:物理硬删,审计只留一行) ---------------------------------------
679
+ /** 物理删除某主体的全部事件并重建投影;删除本身留一行审计事件(红线 6「可删除」)。 */
633
680
  forgetSubject(subjects, actor = 'system:state-cli') {
634
- const run = this.db.transaction(()=>{
681
+ // DELETE events 开头 → 维持 DEFERRED(审计表见 runWrite 上方)。
682
+ const run = this.db.transaction(() => {
635
683
  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);
684
+ for (const s of subjects) {
685
+ const placeholders = s.kinds.map(() => '?').join(',');
686
+ 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
687
  deleted += info.changes;
640
688
  }
641
- this.insertEvent({
642
- actor,
643
- kind: 'forget.executed',
644
- payload: {
645
- subjects
646
- }
647
- });
689
+ this.insertEvent({ actor, kind: 'forget.executed', payload: { subjects } });
648
690
  this.rebuildProjections();
649
- return {
650
- deleted
651
- };
691
+ return { deleted };
652
692
  });
653
693
  return run();
654
694
  }
655
695
  close() {
656
696
  this.db.close();
697
+ for (const [key, value] of openLedgers)
698
+ if (value === this)
699
+ openLedgers.delete(key);
657
700
  }
658
701
  }
702
+ // ---- 打开/迁移(one-shot D2;模块级缓存避免每次读开新句柄) ----------------------
659
703
  const openLedgers = new Map();
660
- function openDb(stateRoot) {
661
- const path = ledgerDbPath(stateRoot);
662
- const cached = openLedgers.get(path);
663
- if (cached) return cached;
664
- mkdirSync(join(path, '..'), {
665
- recursive: true
704
+ /**
705
+ * WAL 切换(D15-1 的第一个抛点)。
706
+ *
707
+ * 切成 WAL 要拿排他锁,而 `PRAGMA journal_mode` 并非在所有路径上都会走 busy
708
+ * handler,所以除了「busy_timeout 先装好」之外还要有界重试。每次失败先回看当前
709
+ * 模式:别的进程已经切成 wal 就直接收工——冷启动竞赛里这是最常见的结局。
710
+ *
711
+ * 不抛而仍未切成 wal(例如不支持 WAL 的文件系统)时保持修复前行为:照旧继续,
712
+ * 不编造「忙」。只有真的抛争用码且重试耗尽才给 LedgerBusyError。
713
+ */
714
+ function ensureWalMode(db) {
715
+ const currentMode = () => String(db.pragma('journal_mode', { simple: true }) ?? '').toLowerCase();
716
+ if (currentMode() === 'wal')
717
+ return;
718
+ withContentionRetry('openDb journal_mode = WAL', () => {
719
+ if (currentMode() === 'wal')
720
+ return; // 竞赛里最常见的结局:别的进程已经切好了
721
+ db.pragma('journal_mode = WAL');
722
+ }, { retries: OPEN_CONTENTION_RETRIES, budgetMs: OPEN_CONTENTION_BUDGET_MS });
723
+ }
724
+ /**
725
+ * 建表/迁移(D15-1 的第二个抛点):整段进 BEGIN IMMEDIATE + 有界重试。
726
+ *
727
+ * 修复前版本探测在事务**外**,两个冷启动进程会各自读到 user_version = 0,再同时
728
+ * 进 DEFERRED 事务执行 `db.exec(SCHEMA)` —— 第一条 DDL 要升级成写,输的那个直接
729
+ * 抛裸 SQLITE_BUSY。现在版本探测与建表同处一个写锁下,后来者在 busy handler 里
730
+ * 排队,进去后重新读到 user_version = 2,于是只跑一遍幂等建表。
731
+ *
732
+ * 返回 isV1:v1→v2 升级后要重建投影,而那需要 StateLedger 实例,只能在事务外做。
733
+ */
734
+ function ensureSchema(db) {
735
+ // 快路径:schema 已是 v2、kv 旗标在位、历史坏迁移已修 → 纯读探测,完全不取写锁。
736
+ // (修复前每次 open 都要写一次 kv schema_version,连只读调用方也在抢写锁,
737
+ // 冷启动竞赛因此更密;值本来就幂等,不写与写同义。)
738
+ if (schemaAlreadyCurrent(db))
739
+ return false;
740
+ const kvSchemaVersion = 'INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v';
741
+ const migrate = db.transaction(() => {
742
+ // 版本探测搬进事务:与建表同处一个写锁下,不再有两个进程各读到 0 的 TOCTOU。
743
+ const version = db.pragma('user_version', { simple: true });
744
+ let isV1 = false;
745
+ if (version < 2) {
746
+ // schema v1→v2 迁移:v1 无 tenant_id 列(user_version 未设置过,实际为 0)
747
+ // 检测是否真有 v1 数据(有 events 表但无 tenant_id 列)
748
+ const hasEventsTable = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='events'").get() !== undefined;
749
+ const eventsHasTenant = hasEventsTable && db.prepare('PRAGMA table_info(events)').all().some(c => c.name === 'tenant_id');
750
+ isV1 = hasEventsTable && !eventsHasTenant;
751
+ if (isV1) {
752
+ // v1 → v2:events 加 tenant_id,回填 'local';投影/工单/pending_writes 强制重建(旧表无 tenant_id)
753
+ db.exec(`
754
+ ALTER TABLE events ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'local';
755
+ DROP INDEX IF EXISTS events_idem;
756
+ CREATE UNIQUE INDEX events_idem ON events(tenant_id, idem_key) WHERE idem_key IS NOT NULL;
757
+ DROP TABLE IF EXISTS projection_docs;
758
+ DROP TABLE IF EXISTS projection_items;
759
+ DROP TABLE IF EXISTS workflow_runs;
760
+ DROP TABLE IF EXISTS workflow_steps;
761
+ DROP TABLE IF EXISTS pending_writes;
762
+ `);
763
+ }
764
+ else {
765
+ // v2 幂等:检查 workflow_runs 是否缺 tenant_id(修一次跑坏的迁移)
766
+ const wrHasTenant = db.prepare('PRAGMA table_info(workflow_runs)').all().some(c => c.name === 'tenant_id');
767
+ if (!wrHasTenant && db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='workflow_runs'").get()) {
768
+ db.exec(`
769
+ DROP TABLE IF EXISTS workflow_runs;
770
+ DROP TABLE IF EXISTS workflow_steps;
771
+ DROP TABLE IF EXISTS pending_writes;
772
+ DROP TABLE IF EXISTS projection_docs;
773
+ DROP TABLE IF EXISTS projection_items;
774
+ `);
775
+ }
776
+ }
777
+ db.exec(SCHEMA);
778
+ db.pragma('user_version = 2');
779
+ db.prepare(kvSchemaVersion).run('schema_version', SCHEMA_VERSION);
780
+ }
781
+ else {
782
+ // 已是 v2:只做幂等建表(IF NOT EXISTS)。坏迁移修补只属于 version < 2 分支,
783
+ // 与修复前一致——user_version 已经是 2 时绝不去 DROP 任何可能有数据的表。
784
+ db.exec(SCHEMA);
785
+ db.prepare(kvSchemaVersion).run('schema_version', SCHEMA_VERSION);
786
+ }
787
+ return isV1;
666
788
  });
667
- mkdirSync(dirname(path), {
668
- recursive: true
789
+ return withContentionRetry('openDb schema ensure/migrate', () => migrate.immediate(), {
790
+ retries: OPEN_CONTENTION_RETRIES,
791
+ budgetMs: OPEN_CONTENTION_BUDGET_MS,
669
792
  });
793
+ }
794
+ /** 当前 SCHEMA 应当存在的全部对象;快路径据此判定「慢路径必然是空操作」。 */
795
+ const SCHEMA_OBJECTS = ['events', 'events_idem', 'projection_docs', 'projection_items', 'workflow_runs', 'workflow_steps', 'pending_writes', 'kv'];
796
+ /**
797
+ * 纯读探测:user_version 已是 2、SCHEMA 全部对象在位、kv 旗标已等于目标值
798
+ * → 慢路径(exec SCHEMA + kv upsert)可证明是空操作,于是整段写锁都省掉。
799
+ * 任何一项不满足(新库,或别的进程正在建而本连接还看不到)→ false,走慢路径。
800
+ */
801
+ function schemaAlreadyCurrent(db) {
802
+ try {
803
+ if (db.pragma('user_version', { simple: true }) < 2)
804
+ return false;
805
+ const present = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type IN ('table', 'index')").all().map(r => r.name));
806
+ if (!SCHEMA_OBJECTS.every(name => present.has(name)))
807
+ return false;
808
+ const row = db.prepare(`SELECT v FROM kv WHERE k = 'schema_version'`).get();
809
+ return row?.v === SCHEMA_VERSION;
810
+ }
811
+ catch {
812
+ return false;
813
+ }
814
+ }
815
+ export function openDb(stateRoot, tenant = 'local') {
816
+ const path = ledgerDbPath(stateRoot);
817
+ const cacheKey = `${path}::${tenant}`;
818
+ const cached = openLedgers.get(cacheKey);
819
+ if (cached)
820
+ return cached;
821
+ mkdirSync(dirname(path), { recursive: true });
670
822
  const db = new Database(path);
671
- db.pragma('journal_mode = WAL');
672
- db.pragma('synchronous = NORMAL');
673
- 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);
823
+ let isV1 = false;
824
+ try {
825
+ // D15-1 顺序红线:busy_timeout 必须在任何可争用语句之前装好。修复前它排在
826
+ // `journal_mode = WAL` 之后,于是 WAL 切换的排他锁被持有时,其余连接还没有
827
+ // busy handler —— 冷启动并发打开直接抛裸 SQLITE_BUSY。顺序不可再调换。
828
+ db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
829
+ ensureWalMode(db);
830
+ db.pragma('synchronous = NORMAL');
831
+ isV1 = ensureSchema(db);
832
+ }
833
+ catch (e) {
834
+ // 打开失败不留半开句柄(重试/上抛都不该泄漏 better-sqlite3 的文件描述符)。
835
+ try {
836
+ db.close();
837
+ }
838
+ catch { /* 句柄已废,关不掉不影响裁决 */ }
839
+ throw e;
840
+ }
841
+ // v1 升级后:旧单用户事件只可安全归入 local;即使调用者打开非 local 租户,也只重建 local 投影。
842
+ if (isV1) {
843
+ const ledger = new StateLedger(db, stateRoot, 'local');
844
+ ledger.rebuildProjections();
845
+ }
846
+ const ledger = new StateLedger(db, stateRoot, tenant);
847
+ openLedgers.set(cacheKey, ledger);
678
848
  return ledger;
679
849
  }
680
- export function openLedgerIfExists(stateRoot) {
681
- if (!ledgerExists(stateRoot)) return null;
682
- return openDb(stateRoot);
850
+ /** 只读场景:账本不存在返回 null(调用方走文件回退) */
851
+ export function openLedgerIfExists(stateRoot, tenant = 'local') {
852
+ if (!ledgerExists(stateRoot))
853
+ return null;
854
+ return openDb(stateRoot, tenant);
683
855
  }
684
- export function ensureLedger(stateRoot) {
685
- if (ledgerExists(stateRoot)) return openDb(stateRoot);
856
+ /**
857
+ * 确保账本可用(写路径必经):
858
+ * - DB 已存在 → 直接用
859
+ * - 无 DB 无旧文件 → 建空账本(fresh root,smoke/测试形态)
860
+ * - 无 DB 有旧文件 → 快照 → 建账本 → 旧数据单事务导入为 events(one-shot;
861
+ * 导入幂等键 + kv 旗标双保险,并发 ensure 不重复导入)
862
+ */
863
+ export function ensureLedger(stateRoot, tenant = 'local') {
864
+ if (ledgerExists(stateRoot))
865
+ return openDb(stateRoot, tenant);
686
866
  const dir = stateDirOf(stateRoot);
687
867
  const hasLegacy = legacyFilesPresent(stateRoot);
688
868
  let backupDir = null;
689
869
  if (hasLegacy) {
690
870
  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));
871
+ mkdirSync(backupDir, { recursive: true });
872
+ for (const f of LEGACY_FILES) {
873
+ if (existsSync(join(dir, f)))
874
+ copyFileSync(join(dir, f), join(backupDir, f));
696
875
  }
697
876
  }
698
- const ledger = openDb(stateRoot);
699
- if (hasLegacy) importLegacyInto(ledger, dir);
700
- return ledger;
877
+ if (hasLegacy) {
878
+ // 旧 JSON/JSONL 是单用户本地状态,没有可审计租户归属;迁移边界=只归入 local,绝不按调用 tenant 猜测或搬移。
879
+ const localLedger = openDb(stateRoot, 'local');
880
+ importLegacyInto(localLedger, dir);
881
+ return tenant === 'local' ? localLedger : openDb(stateRoot, tenant);
882
+ }
883
+ return openDb(stateRoot, tenant);
701
884
  }
702
885
  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(()=>{
886
+ const imported = { motivation: false, wishes: 0, utilityEvents: 0, trips: 0, companions: 0 };
887
+ // 读改写:以 SELECT kv.legacy_imported_v1 开头(旗标判定)→ IMMEDIATE + 有界重试。
888
+ // 「并发 ensure 不重复导入」靠的就是这个旗标,而 DEFERRED 事务下两个进程会同时
889
+ // 读到无旗标、再在写时一个拿锁一个抛 SQLITE_BUSY_SNAPSHOT —— 正是 D15-2 的形状。
890
+ const run = ledger.db.transaction(() => {
891
+ // 重试要幂等:计数器属于「本次事务的成果」,每次尝试重新归零,回滚过的那次
892
+ // 不得残留在报告里。
893
+ Object.assign(imported, { motivation: false, wishes: 0, utilityEvents: 0, trips: 0, companions: 0 });
711
894
  const flag = ledger.db.prepare(`SELECT v FROM kv WHERE k = 'legacy_imported_v1'`).get();
712
- if (flag) return;
713
- const readJsonSafe = (p)=>{
895
+ if (flag)
896
+ return;
897
+ const readJsonSafe = (p) => {
714
898
  try {
715
899
  return JSON.parse(readFileSync(p, 'utf-8'));
716
- } catch {
900
+ }
901
+ catch {
717
902
  return null;
718
903
  }
719
904
  };
905
+ // 动机画像:整档快照事件(fold 直接落投影)
720
906
  const profile = readJsonSafe(join(dir, 'motivation-profile.json'));
721
907
  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;
908
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'motivation.imported', subjectId: 'motivation', payload: { profile }, idemKey: 'import:motivation' }) !== null)
909
+ imported.motivation = true;
731
910
  }
911
+ // 愿望池:逐条导入(保留原 wish_id)
732
912
  const pool = readJsonSafe(join(dir, 'wish-pool.json'));
733
913
  if (Array.isArray(pool)) {
734
- for (const w of pool){
914
+ for (const w of pool) {
735
915
  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++;
916
+ const wish = { ...w, wish_id: id };
917
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'wish.imported', subjectId: id, payload: { wish }, idemKey: `import:wish:${id}` }) !== null)
918
+ imported.wishes++;
749
919
  }
750
920
  }
921
+ // 效用流:逐行导入(语义 event_id 即幂等键)
751
922
  try {
752
923
  const lines = readFileSync(join(dir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean);
753
- for (const line of lines){
924
+ for (const line of lines) {
754
925
  try {
755
926
  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 {}
927
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'memory_utility.event', subjectId: ev.wish_id, payload: { event: ev }, idemKey: `mu:${ev.event_id}` }) !== null)
928
+ imported.utilityEvents++;
929
+ }
930
+ catch { /* 脏行跳过:导入永不因单行失败而中断 */ }
766
931
  }
767
- } catch {}
932
+ }
933
+ catch { /* 文件不存在 */ }
934
+ // 时间线:逐行导入(trip_id 幂等)
768
935
  try {
769
936
  const lines = readFileSync(join(dir, 'trips.jsonl'), 'utf-8').split('\n').filter(Boolean);
770
- for (const line of lines){
937
+ for (const line of lines) {
771
938
  try {
772
939
  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 {}
940
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'trip.logged', subjectId: trip.trip_id, payload: { trip }, idemKey: `trip:${trip.trip_id}` }) !== null)
941
+ imported.trips++;
942
+ }
943
+ catch { /* 脏行跳过 */ }
783
944
  }
784
- } catch {}
945
+ }
946
+ catch { /* 文件不存在 */ }
947
+ // 同行人:逐条导入(保留 companion_id)
785
948
  const companions = readJsonSafe(join(dir, 'companions.json'));
786
949
  if (Array.isArray(companions)) {
787
- for (const c of companions){
950
+ for (const c of companions) {
788
951
  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++;
952
+ if (!id)
953
+ continue;
954
+ if (ledger.insertEvent({ actor: 'system:migrate', kind: 'companion.imported', subjectId: id, payload: { profile: c }, idemKey: `import:companion:${id}` }) !== null)
955
+ imported.companions++;
799
956
  }
800
957
  }
801
958
  ledger.rebuildProjections();
802
959
  ledger.db.prepare(`INSERT INTO kv (k, v) VALUES ('legacy_imported_v1', 'done') ON CONFLICT(k) DO UPDATE SET v = excluded.v`).run();
803
960
  });
804
- run();
961
+ withContentionRetry('ensureLedger legacy import', () => run.immediate());
805
962
  return imported;
806
963
  }
964
+ // ---- 读路径统一出口:账本优先,未迁移 root 回退旧文件(只读,绝不反向写) ---------
807
965
  function readLegacyJson(stateRoot, name, fallback) {
808
966
  try {
809
967
  return JSON.parse(readFileSync(join(stateDirOf(stateRoot), name), 'utf-8'));
810
- } catch {
968
+ }
969
+ catch {
811
970
  return fallback;
812
971
  }
813
972
  }
814
973
  function readLegacyJsonl(stateRoot, name) {
815
974
  try {
816
- return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
817
- } catch {
975
+ return readFileSync(join(stateDirOf(stateRoot), name), 'utf-8').split('\n').filter(Boolean).map(l => JSON.parse(l));
976
+ }
977
+ catch {
818
978
  return [];
819
979
  }
820
980
  }
821
- export function readMotivationWithFallback(stateRoot) {
822
- const ledger = openLedgerIfExists(stateRoot);
823
- if (ledger) return ledger.readMotivation();
981
+ export function readMotivationWithFallback(stateRoot, tenant = 'local') {
982
+ const ledger = openLedgerIfExists(stateRoot, tenant);
983
+ if (ledger)
984
+ return ledger.readMotivation();
985
+ if (tenant !== 'local')
986
+ return null;
824
987
  return readLegacyJson(stateRoot, 'motivation-profile.json', null);
825
988
  }
826
- export function readWishPoolWithFallback(stateRoot) {
827
- const ledger = openLedgerIfExists(stateRoot);
828
- if (ledger) return ledger.readWishPool();
989
+ export function readWishPoolWithFallback(stateRoot, tenant = 'local') {
990
+ const ledger = openLedgerIfExists(stateRoot, tenant);
991
+ if (ledger)
992
+ return ledger.readWishPool();
993
+ if (tenant !== 'local')
994
+ return [];
829
995
  return readLegacyJson(stateRoot, 'wish-pool.json', []);
830
996
  }
831
- export function readUtilityEventsWithFallback(stateRoot) {
832
- const ledger = openLedgerIfExists(stateRoot);
833
- if (ledger) return ledger.readUtilityEvents();
997
+ export function readUtilityEventsWithFallback(stateRoot, tenant = 'local') {
998
+ const ledger = openLedgerIfExists(stateRoot, tenant);
999
+ if (ledger)
1000
+ return ledger.readUtilityEvents();
1001
+ if (tenant !== 'local')
1002
+ return [];
834
1003
  return readLegacyJsonl(stateRoot, 'memory-utility.jsonl');
835
1004
  }
836
- export function readTripsWithFallback(stateRoot) {
837
- const ledger = openLedgerIfExists(stateRoot);
838
- if (ledger) return ledger.readTrips();
1005
+ export function readTripsWithFallback(stateRoot, tenant = 'local') {
1006
+ const ledger = openLedgerIfExists(stateRoot, tenant);
1007
+ if (ledger)
1008
+ return ledger.readTrips();
1009
+ if (tenant !== 'local')
1010
+ return [];
839
1011
  return readLegacyJsonl(stateRoot, 'trips.jsonl');
840
1012
  }
841
- export function readCompanionsWithFallback(stateRoot) {
842
- const ledger = openLedgerIfExists(stateRoot);
843
- if (ledger) return ledger.readCompanions();
1013
+ export function readCompanionsWithFallback(stateRoot, tenant = 'local') {
1014
+ const ledger = openLedgerIfExists(stateRoot, tenant);
1015
+ if (ledger)
1016
+ return ledger.readCompanions();
1017
+ if (tenant !== 'local')
1018
+ return [];
844
1019
  return readLegacyJson(stateRoot, 'companions.json', []);
845
1020
  }
846
-
847
-
848
- //# sourceURL=/Users/bytedance/work/gotry/ts/src/state-ledger.ts