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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/README.md +102 -194
  2. package/README.zh-CN.md +115 -210
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-bootstrap.js +466 -35
  5. package/bin/gotry-inner.js +255 -36
  6. package/bin/gotry-process-liveness.d.ts +27 -0
  7. package/bin/gotry-process-liveness.js +97 -0
  8. package/bin/gotry-runtime-resolution.d.ts +1 -1
  9. package/bin/gotry-runtime-resolution.js +2 -2
  10. package/client/client.js +174 -0
  11. package/cordis.gotry-patch.yml +70 -93
  12. package/dist/capabilities/agent-reach-deep.js +45 -122
  13. package/dist/capabilities/agent-reach.js +63 -148
  14. package/dist/capabilities/anything.js +89 -75
  15. package/dist/capabilities/artifacts.js +123 -104
  16. package/dist/capabilities/channel-health.js +65 -45
  17. package/dist/capabilities/channel-registry.js +108 -87
  18. package/dist/capabilities/doctor.js +357 -235
  19. package/dist/capabilities/effect.js +223 -309
  20. package/dist/capabilities/fact-log.js +28 -14
  21. package/dist/capabilities/flyai.js +245 -156
  22. package/dist/capabilities/ground-transfer.js +509 -0
  23. package/dist/capabilities/hbcli.js +144 -148
  24. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  25. package/dist/capabilities/incident-log.js +131 -61
  26. package/dist/capabilities/model-override.js +30 -10
  27. package/dist/capabilities/opensky.js +54 -43
  28. package/dist/capabilities/resilience.js +50 -32
  29. package/dist/capabilities/session/action-cache.js +50 -44
  30. package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
  31. package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
  32. package/dist/capabilities/session/adapters/dida-portal.js +206 -0
  33. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  34. package/dist/capabilities/session/adapters/rail-12306.js +169 -112
  35. package/dist/capabilities/session/benchmark.js +85 -149
  36. package/dist/capabilities/session/extension-bridge.js +320 -259
  37. package/dist/capabilities/session/extension-channel.js +57 -76
  38. package/dist/capabilities/session/extension-distribution.js +115 -119
  39. package/dist/capabilities/session/extract.js +31 -21
  40. package/dist/capabilities/session/golden-score.js +55 -33
  41. package/dist/capabilities/session/health-watch.js +146 -154
  42. package/dist/capabilities/session/read-guard.js +55 -47
  43. package/dist/capabilities/session/static-flight-golden.js +49 -36
  44. package/dist/capabilities/session/transport.js +54 -61
  45. package/dist/capabilities/session/wizard.js +63 -93
  46. package/dist/capabilities/session-consent.js +143 -47
  47. package/dist/capabilities/session-login.js +95 -126
  48. package/dist/capabilities/session-search.js +391 -253
  49. package/dist/capabilities/sponsor-plugin.js +341 -0
  50. package/dist/capabilities/visa-policy.js +87 -73
  51. package/dist/capabilities/weather.js +163 -235
  52. package/dist/scripts/action-cache-tests.js +24 -59
  53. package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
  54. package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
  55. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  56. package/dist/scripts/agent-reach-tests.js +26 -18
  57. package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
  58. package/dist/scripts/anything-tests.js +58 -62
  59. package/dist/scripts/artifact-client-contract-tests.js +69 -0
  60. package/dist/scripts/artifacts-capability-tests.js +235 -0
  61. package/dist/scripts/async-collect.js +26 -15
  62. package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
  63. package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
  64. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
  65. package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
  66. package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
  67. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
  68. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
  69. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
  70. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
  71. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
  72. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
  73. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
  74. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
  75. package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
  76. package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
  77. package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
  78. package/dist/scripts/booking-executor-tests.js +80 -0
  79. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  80. package/dist/scripts/booking-saga-tests.js +61 -123
  81. package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
  82. package/dist/scripts/bootstrap-tests.js +1383 -128
  83. package/dist/scripts/build-changelog.js +89 -130
  84. package/dist/scripts/build-metrics-report.js +135 -143
  85. package/dist/scripts/changelog-tests.js +35 -82
  86. package/dist/scripts/channel-probe-tests.js +44 -116
  87. package/dist/scripts/channel-probe.js +84 -167
  88. package/dist/scripts/channel-registry-tests.js +97 -185
  89. package/dist/scripts/companion-tests.js +25 -70
  90. package/dist/scripts/dida-cookie-probe.js +87 -0
  91. package/dist/scripts/dida-sw-probe.js +114 -0
  92. package/dist/scripts/diff-test.js +17 -12
  93. package/dist/scripts/doctor-tests.js +428 -120
  94. package/dist/scripts/dsh-artifact-e2e.js +150 -0
  95. package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
  96. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  97. package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
  98. package/dist/scripts/dsh-runtime-closure.js +75 -63
  99. package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
  100. package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
  101. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  102. package/dist/scripts/effect-tests.js +165 -552
  103. package/dist/scripts/engine-run.js +1 -3
  104. package/dist/scripts/engine-tests.js +16 -10
  105. package/dist/scripts/evaluation-cadence-tests.js +69 -242
  106. package/dist/scripts/evaluation-contract-tests.js +193 -499
  107. package/dist/scripts/extension-distribution-cli.js +19 -15
  108. package/dist/scripts/extension-distribution-tests.js +98 -211
  109. package/dist/scripts/extension-tests.js +366 -389
  110. package/dist/scripts/fact-gate-tests.js +725 -263
  111. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  112. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  113. package/dist/scripts/flight-malformed-tests.js +159 -0
  114. package/dist/scripts/flyai-tests.js +325 -68
  115. package/dist/scripts/g5-guard-tests.js +131 -0
  116. package/dist/scripts/g5-guard.js +189 -0
  117. package/dist/scripts/gen-lunar-anchors.js +106 -0
  118. package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
  119. package/dist/scripts/ground-transfer-tests.js +956 -0
  120. package/dist/scripts/hbcli-e2e-tests.js +71 -90
  121. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  122. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  123. package/dist/scripts/hbcli-tests.js +41 -115
  124. package/dist/scripts/health-watch-cli.js +42 -28
  125. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  126. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  127. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  128. package/dist/scripts/i18n-tests.js +16 -9
  129. package/dist/scripts/incident-tests.js +281 -87
  130. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  131. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  132. package/dist/scripts/issue-271-liveness-tests.js +547 -0
  133. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  134. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  135. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  136. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  137. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  138. package/dist/scripts/journey-tests.js +22 -27
  139. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  140. package/dist/scripts/kernel-manifest-gate.js +126 -0
  141. package/dist/scripts/kernel-manifest-tests.js +212 -0
  142. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  143. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  144. package/dist/scripts/kernel-manifest.js +369 -0
  145. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  146. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  147. package/dist/scripts/ledger-tests.js +325 -340
  148. package/dist/scripts/ledger-workflow-crash.js +10 -20
  149. package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
  150. package/dist/scripts/memory-capture-tests.js +242 -61
  151. package/dist/scripts/memory-decay-tests.js +18 -52
  152. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  153. package/dist/scripts/memory-lifecycle.js +196 -0
  154. package/dist/scripts/memory-metrics.js +24 -12
  155. package/dist/scripts/memory-value-report.js +474 -222
  156. package/dist/scripts/memory-value-tests.js +328 -0
  157. package/dist/scripts/metrics-report-tests.js +62 -199
  158. package/dist/scripts/model-override-e2e.js +66 -105
  159. package/dist/scripts/nightly-evidence-tests.js +64 -89
  160. package/dist/scripts/nightly-evidence.js +108 -111
  161. package/dist/scripts/nudge-digest.js +39 -28
  162. package/dist/scripts/onboarding-tests.js +63 -88
  163. package/dist/scripts/opensky-check.js +18 -13
  164. package/dist/scripts/opensky-tests.js +28 -28
  165. package/dist/scripts/persona-surface-guard-tests.js +37 -12
  166. package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
  167. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  168. package/dist/scripts/price-drift-tests.js +126 -227
  169. package/dist/scripts/price-drift-watch.js +209 -253
  170. package/dist/scripts/probe-poi-tests.js +52 -45
  171. package/dist/scripts/product-metrics.js +187 -273
  172. package/dist/scripts/publish-preverify.js +47 -41
  173. package/dist/scripts/realtime-pricing-tests.js +37 -76
  174. package/dist/scripts/replay-async.js +20 -18
  175. package/dist/scripts/replay-real.js +16 -18
  176. package/dist/scripts/replay.js +52 -82
  177. package/dist/scripts/session-attach-diagnose.js +16 -12
  178. package/dist/scripts/session-attach-poc.js +34 -37
  179. package/dist/scripts/session-benchmark.js +66 -147
  180. package/dist/scripts/session-dida-live-e2e.js +151 -0
  181. package/dist/scripts/session-extract-tests.js +14 -35
  182. package/dist/scripts/session-login.js +14 -13
  183. package/dist/scripts/session-tests.js +545 -583
  184. package/dist/scripts/sf-live-benchmark.js +92 -110
  185. package/dist/scripts/sf-live-cli-tests.js +2 -9
  186. package/dist/scripts/sf-soft-score-tests.js +19 -60
  187. package/dist/scripts/sf-summary-tests.js +149 -0
  188. package/dist/scripts/sf-summary.js +466 -84
  189. package/dist/scripts/skeleton-check.js +16 -14
  190. package/dist/scripts/skeleton-integration-test.js +11 -10
  191. package/dist/scripts/skills-contract-tests.js +51 -46
  192. package/dist/scripts/smoke-session-gate-tests.js +12 -20
  193. package/dist/scripts/smoke.js +541 -422
  194. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  195. package/dist/scripts/state-cli-tests.js +226 -101
  196. package/dist/scripts/state-cli.js +549 -212
  197. package/dist/scripts/static-golden-tests.js +46 -180
  198. package/dist/scripts/time-eval-tests.js +210 -192
  199. package/dist/scripts/travel-timeline-tests.js +29 -93
  200. package/dist/scripts/turn-handoff-collect-tests.js +46 -93
  201. package/dist/scripts/turn-handoff-collect.js +71 -99
  202. package/dist/scripts/turn-policy-tests.js +28 -29
  203. package/dist/scripts/typed-contract-canary.js +77 -162
  204. package/dist/scripts/tz-resolver-sanity.js +50 -0
  205. package/dist/scripts/tz-resolver-tests.js +584 -0
  206. package/dist/scripts/unified-tests.js +23 -19
  207. package/dist/scripts/visa-policy-tests.js +31 -44
  208. package/dist/scripts/weather-tests.js +121 -543
  209. package/dist/scripts/wish-channel-gate-tests.js +29 -86
  210. package/dist/scripts/write-gate-tests.js +522 -0
  211. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  212. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  213. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  214. package/dist/scripts/z3-race-tests.js +18 -12
  215. package/dist/src/artifact-gate.js +652 -221
  216. package/dist/src/backend/kernel.js +91 -0
  217. package/dist/src/backend/modules/booking-copilot.js +55 -0
  218. package/dist/src/backend/modules/booking-executor.js +124 -0
  219. package/dist/src/backend/modules/session-search.js +194 -0
  220. package/dist/src/benchmark-agent-conformance.js +296 -262
  221. package/dist/src/benchmark-environment-bridge.js +298 -413
  222. package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
  223. package/dist/src/benchmark-tool-isolation.js +94 -67
  224. package/dist/src/bookable-facts.js +299 -174
  225. package/dist/src/booking-saga.js +64 -120
  226. package/dist/src/booking-surface/availability-policy.js +443 -576
  227. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  228. package/dist/src/booking-surface/contracts.js +24 -80
  229. package/dist/src/booking-surface/dsh-planner.js +510 -281
  230. package/dist/src/booking-surface/dsh-plugin.js +17 -1
  231. package/dist/src/booking-surface/error-codes.js +82 -10
  232. package/dist/src/booking-surface/index.js +0 -3
  233. package/dist/src/booking-surface/profile.js +10 -54
  234. package/dist/src/booking-surface/recovery-chain.js +367 -0
  235. package/dist/src/booking-surface/runtime.js +765 -891
  236. package/dist/src/booking-surface/server.js +200 -251
  237. package/dist/src/booking-surface/startup.js +58 -54
  238. package/dist/src/booking-surface/validation.js +150 -218
  239. package/dist/src/bridge.js +20 -13
  240. package/dist/src/companions.js +48 -74
  241. package/dist/src/contracts.js +21 -43
  242. package/dist/src/dsh-llm.js +82 -120
  243. package/dist/src/engine.js +253 -239
  244. package/dist/src/evaluation-cadence.js +72 -209
  245. package/dist/src/evaluation-contracts.js +220 -843
  246. package/dist/src/flight-pack-adapter.js +54 -0
  247. package/dist/src/flight-pack-contract.js +2 -0
  248. package/dist/src/gotry-backend.js +70 -0
  249. package/dist/src/hotel-date-gate.js +118 -0
  250. package/dist/src/i18n.js +18 -7
  251. package/dist/src/index.js +1425 -1629
  252. package/dist/src/journey.js +132 -81
  253. package/dist/src/ledger-repair-apply.js +538 -0
  254. package/dist/src/ledger-repair-plan.js +463 -0
  255. package/dist/src/loop.js +257 -166
  256. package/dist/src/memory-capture.js +180 -32
  257. package/dist/src/memory-decay.js +30 -14
  258. package/dist/src/memory-lifecycle.js +1310 -0
  259. package/dist/src/memory-utility.js +27 -40
  260. package/dist/src/mock-llm.js +36 -61
  261. package/dist/src/model.js +81 -36
  262. package/dist/src/realtime-pricing.js +63 -72
  263. package/dist/src/slot-spec.js +84 -106
  264. package/dist/src/state-ledger.js +310 -471
  265. package/dist/src/subagent-job-id-guard.js +58 -0
  266. package/dist/src/time-anchor.js +231 -33
  267. package/dist/src/tool-packet.js +20 -8
  268. package/dist/src/travel-slots.js +58 -35
  269. package/dist/src/travel-timeline.js +44 -63
  270. package/dist/src/turn-deadline.js +121 -127
  271. package/dist/src/turn-policy.js +61 -89
  272. package/dist/src/tz-resolver.js +143 -0
  273. package/dist/src/unified.js +504 -381
  274. package/dist/src/wish-pool.js +39 -14
  275. package/dist/src/write-gate.js +495 -0
  276. package/dist/src/z3-shared.js +273 -7
  277. package/extension/README.md +4 -3
  278. package/extension/background.js +125 -14
  279. package/extension/content-bridge.js +23 -1
  280. package/extension/content-main.js +15 -4
  281. package/extension/manifest.json +16 -8
  282. package/package.json +251 -220
  283. package/ts/capabilities/agent-reach.ts +4 -4
  284. package/ts/capabilities/anything.ts +1 -1
  285. package/ts/capabilities/artifacts.ts +46 -9
  286. package/ts/capabilities/flyai.ts +156 -37
  287. package/ts/capabilities/hbcli.ts +1 -1
  288. package/ts/capabilities/incident-log.ts +88 -59
  289. package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
  290. package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
  291. package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
  292. package/ts/capabilities/session/extension-bridge.ts +226 -101
  293. package/ts/capabilities/session/extension-channel.ts +22 -9
  294. package/ts/capabilities/session/wizard.ts +2 -2
  295. package/ts/capabilities/session-consent.ts +98 -14
  296. package/ts/capabilities/session-login.ts +1 -0
  297. package/ts/capabilities/session-search.ts +315 -31
  298. package/ts/dsh-runtime/vendor/README.md +8 -4
  299. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
  300. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
  301. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
  302. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
  303. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
  304. package/ts/package.json +236 -16
  305. package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
  306. package/ts/scripts/state-cli.ts +418 -51
  307. package/ts/src/artifact-gate.ts +561 -15
  308. package/ts/src/benchmark-environment-bridge.ts +37 -20
  309. package/ts/src/bookable-facts.ts +183 -7
  310. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  311. package/ts/src/booking-surface/dsh-planner.ts +240 -32
  312. package/ts/src/booking-surface/dsh-plugin.js +17 -1
  313. package/ts/src/booking-surface/error-codes.ts +75 -0
  314. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  315. package/ts/src/booking-surface/server.ts +84 -51
  316. package/ts/src/booking-surface/validation.ts +2 -0
  317. package/ts/src/contracts.ts +12 -2
  318. package/ts/src/dsh-llm.ts +4 -8
  319. package/ts/src/index.ts +609 -90
  320. package/ts/src/loop.ts +116 -7
  321. package/ts/src/memory-capture.ts +227 -3
  322. package/ts/src/mock-llm.ts +4 -8
  323. package/ts/src/model.ts +88 -2
  324. package/ts/src/state-ledger.ts +47 -29
  325. package/ts/src/subagent-job-id-guard.ts +72 -0
  326. package/ts/src/time-anchor.ts +217 -13
  327. package/ts/src/unified.ts +316 -54
@@ -1,7 +1,8 @@
1
+ /** Durable, task-scoped runtime for the embedded Booking Copilot. */
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
- import { BOOKING_READ_ACTION_KINDS, BOOKING_COPILOT_MAX_OPERATIONS } from './contracts.js';
3
- import { validateApprovalAgainstBlocker, validateCriterionBlocker, validateBookingReadAction, validateBookingSurfaceEvent, validateBookingSurface, isBookingDateTime, isSafeBookingRequestKey } from './validation.js';
4
- import { assertWorkspaceLoadedOfferRefsUnique, canIssueOfferCheck, createAvailabilityPolicy, reduceAvailabilityAction, reduceAvailabilityReceipt, validateOffersReceiptWorkspace, availabilityPolicyResult, validateAvailabilityPolicy } from './availability-policy.js';
3
+ import { BOOKING_COPILOT_MAX_OPERATIONS, } from './contracts.js';
4
+ import { validateApprovalAgainstBlocker, validateCriterionBlocker, validateBookingReadAction, validateBookingSurfaceEvent, validateBookingSurface, isBookingDateTime, isSafeBookingRequestKey, } from './validation.js';
5
+ import { assertWorkspaceLoadedOfferRefsUnique, canIssueOfferCheck, createAvailabilityPolicy, reduceAvailabilityAction, reduceAvailabilityReceipt, validateOffersReceiptWorkspace, availabilityPolicyResult, validateAvailabilityPolicy, } from './availability-policy.js';
5
6
  import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.js';
6
7
  const LEDGER_SCHEMA = 'booking.copilot.ledger';
7
8
  const ACTOR = 'system:booking-copilot';
@@ -16,10 +17,13 @@ const APPROVAL_CONSUMED = 'booking.copilot.approval.consumed';
16
17
  const DECISION_BATCH = 'booking.copilot.decision.batch';
17
18
  const REQUEST_BINDING = 'booking.copilot.request.binding';
18
19
  function stable(value) {
19
- if (Array.isArray(value)) return value.map(stable);
20
- if (!value || typeof value !== 'object') return value;
20
+ if (Array.isArray(value))
21
+ return value.map(stable);
22
+ if (!value || typeof value !== 'object')
23
+ return value;
21
24
  const out = {};
22
- for (const key of Object.keys(value).sort())out[key] = stable(value[key]);
25
+ for (const key of Object.keys(value).sort())
26
+ out[key] = stable(value[key]);
23
27
  return out;
24
28
  }
25
29
  export function bookingDigest(value) {
@@ -36,9 +40,7 @@ function bookingWorkspaceSemanticDigest(workspace) {
36
40
  export function bookingTurnDigest(turn) {
37
41
  return bookingDigest(turn);
38
42
  }
39
- function canonicalReceiptDigest(value) {
40
- return bookingDigest(value);
41
- }
43
+ function canonicalReceiptDigest(value) { return bookingDigest(value); }
42
44
  function replayDigestMatches(recordedDigest, raw, normalized) {
43
45
  return typeof recordedDigest === 'string' && (recordedDigest === bookingDigest(raw) || recordedDigest === bookingDigest(normalized));
44
46
  }
@@ -51,7 +53,8 @@ function replayWorkspaceSemanticDigestMatches(recordedDigest, raw, normalized) {
51
53
  function rememberReplayWorkspaceDigest(digests, recordedDigest, raw, normalized) {
52
54
  const normalizedDigest = bookingWorkspaceDigest(normalized);
53
55
  digests.set(bookingWorkspaceDigest(raw), normalizedDigest);
54
- if (typeof recordedDigest === 'string') digests.set(recordedDigest, normalizedDigest);
56
+ if (typeof recordedDigest === 'string')
57
+ digests.set(recordedDigest, normalizedDigest);
55
58
  }
56
59
  function legacyOfferVersionRef(contextRef, offer) {
57
60
  return `legacy-offer-version:${bookingDigest({
@@ -59,99 +62,99 @@ function legacyOfferVersionRef(contextRef, offer) {
59
62
  offerRef: offer.offerRef,
60
63
  hotelRef: offer.hotelRef,
61
64
  evidenceLevel: offer.evidenceLevel,
62
- factRefs: Array.isArray(offer.factRefs) ? offer.factRefs : []
65
+ factRefs: Array.isArray(offer.factRefs) ? offer.factRefs : [],
63
66
  }).slice(0, 40)}`;
64
67
  }
65
68
  function normalizeWorkspaceForReplay(raw) {
66
69
  const candidate = raw;
67
- if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Array.isArray(candidate.loadedOffers) || typeof candidate.contextRef !== 'string') throw new Error('booking_malformed_workspace');
70
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw) || !Array.isArray(candidate.loadedOffers) || typeof candidate.contextRef !== 'string')
71
+ throw new Error('booking_malformed_workspace');
68
72
  const workspace = structuredClone(raw);
69
- workspace.loadedOffers = workspace.loadedOffers.map((offer)=>offer.offerVersionRef ? offer : {
70
- ...offer,
71
- offerVersionRef: legacyOfferVersionRef(workspace.contextRef, offer)
72
- });
73
+ workspace.loadedOffers = workspace.loadedOffers.map((offer) => offer.offerVersionRef ? offer : { ...offer, offerVersionRef: legacyOfferVersionRef(workspace.contextRef, offer) });
73
74
  delete workspace.verifiedOfferRef;
74
75
  return workspace;
75
76
  }
76
77
  function resolveReplayOfferVersion(offerRef, workspace, action, actionId, priorAvailability) {
77
- const priorAttempt = actionId ? priorAvailability?.attempts.find((attempt)=>attempt.actionId === actionId) : undefined;
78
- if (priorAttempt?.offerVersionRef) return priorAttempt.offerVersionRef;
79
- if (action?.kind === 'offer.check' && action.actionId === actionId && action.input.offerRef === offerRef && typeof action.input.offerVersionRef === 'string') return action.input.offerVersionRef;
80
- const match = workspace?.loadedOffers.find((offer)=>offer.offerRef === offerRef);
81
- if (match?.offerVersionRef) return match.offerVersionRef;
78
+ const priorAttempt = actionId ? priorAvailability?.attempts.find((attempt) => attempt.actionId === actionId) : undefined;
79
+ if (priorAttempt?.offerVersionRef)
80
+ return priorAttempt.offerVersionRef;
81
+ if (action?.kind === 'offer.check' && action.actionId === actionId && action.input.offerRef === offerRef && typeof action.input.offerVersionRef === 'string')
82
+ return action.input.offerVersionRef;
83
+ const match = workspace?.loadedOffers.find((offer) => offer.offerRef === offerRef);
84
+ if (match?.offerVersionRef)
85
+ return match.offerVersionRef;
82
86
  throw new Error('booking_replan_required:offer_version_unbound');
83
87
  }
84
88
  function normalizeAvailabilityPolicyForReplay(raw, workspace, action, priorAvailability, workspaceDigestMap = new Map(), receipt) {
85
89
  const normalized = structuredClone(raw);
86
90
  if (normalized.hotels && typeof normalized.hotels === 'object') {
87
- for (const [hotelRef, hotel] of Object.entries(normalized.hotels)){
91
+ for (const [hotelRef, hotel] of Object.entries(normalized.hotels)) {
88
92
  const rawHotel = raw.hotels?.[hotelRef];
89
93
  const priorHotel = priorAvailability?.hotels[hotelRef];
90
- if (!Array.isArray(hotel.tombstonedOfferRefs)) hotel.tombstonedOfferRefs = priorHotel ? [
91
- ...priorHotel.tombstonedOfferRefs
92
- ] : [];
93
- if (!Array.isArray(hotel.tombstonedOfferVersionRefs)) hotel.tombstonedOfferVersionRefs = priorHotel ? [
94
- ...priorHotel.tombstonedOfferVersionRefs
95
- ] : [];
94
+ if (!Array.isArray(hotel.tombstonedOfferRefs))
95
+ hotel.tombstonedOfferRefs = priorHotel ? [...priorHotel.tombstonedOfferRefs] : [];
96
+ if (!Array.isArray(hotel.tombstonedOfferVersionRefs))
97
+ hotel.tombstonedOfferVersionRefs = priorHotel ? [...priorHotel.tombstonedOfferVersionRefs] : [];
96
98
  if (rawHotel && Array.isArray(rawHotel.tombstonedOfferRefs) && priorHotel) {
97
- for (const ref of priorHotel.tombstonedOfferRefs)if (!hotel.tombstonedOfferRefs.includes(ref)) throw new Error('booking_replan_required:tombstone_regression');
99
+ for (const ref of priorHotel.tombstonedOfferRefs)
100
+ if (!hotel.tombstonedOfferRefs.includes(ref))
101
+ throw new Error('booking_replan_required:tombstone_regression');
98
102
  }
99
103
  if (rawHotel && Array.isArray(rawHotel.tombstonedOfferVersionRefs) && priorHotel) {
100
- for (const ref of priorHotel.tombstonedOfferVersionRefs)if (!hotel.tombstonedOfferVersionRefs.includes(ref)) throw new Error('booking_replan_required:tombstone_regression');
104
+ for (const ref of priorHotel.tombstonedOfferVersionRefs)
105
+ if (!hotel.tombstonedOfferVersionRefs.includes(ref))
106
+ throw new Error('booking_replan_required:tombstone_regression');
101
107
  }
102
108
  const source = hotel.currentGeneration?.source;
103
109
  if (source?.kind === 'workspace_snapshot') {
104
110
  const mappedDigest = workspaceDigestMap.get(source.workspaceDigest);
105
- if (mappedDigest) source.workspaceDigest = mappedDigest;
111
+ if (mappedDigest)
112
+ source.workspaceDigest = mappedDigest;
106
113
  }
107
114
  }
108
115
  }
109
116
  if (Array.isArray(normalized.attempts)) {
110
- normalized.attempts = normalized.attempts.map((attempt)=>attempt.offerVersionRef ? attempt : {
111
- ...attempt,
112
- offerVersionRef: resolveReplayOfferVersion(attempt.offerRef, workspace, action, attempt.actionId, priorAvailability)
113
- });
117
+ normalized.attempts = normalized.attempts.map((attempt) => attempt.offerVersionRef ? attempt : { ...attempt, offerVersionRef: resolveReplayOfferVersion(attempt.offerRef, workspace, action, attempt.actionId, priorAvailability) });
114
118
  }
115
119
  const availability = receipt?.observation.kind === 'offer.availability' ? receipt.observation : undefined;
116
120
  if (receipt && action?.kind === 'offer.check' && availability && availability.offerRef === action.input.offerRef && availability.checkedOfferVersionRef === action.input.offerVersionRef) {
117
- const attempt = normalized.attempts.find((candidate)=>candidate.actionId === action.actionId);
121
+ const attempt = normalized.attempts.find((candidate) => candidate.actionId === action.actionId);
118
122
  const hotel = attempt ? normalized.hotels[attempt.hotelRef] : undefined;
119
123
  if (hotel) {
120
124
  if (receipt.status === 'changed' && availability.available === true && availability.currentOfferVersionRef && availability.currentOfferVersionRef !== action.input.offerVersionRef) {
121
- if (!hotel.tombstonedOfferVersionRefs.includes(action.input.offerVersionRef)) hotel.tombstonedOfferVersionRefs.push(action.input.offerVersionRef);
125
+ if (!hotel.tombstonedOfferVersionRefs.includes(action.input.offerVersionRef))
126
+ hotel.tombstonedOfferVersionRefs.push(action.input.offerVersionRef);
122
127
  }
123
- const completeNegative = (receipt.status === 'unavailable' || receipt.status === 'no_match') && receipt.resultContract.outcome === 'empty' && receipt.resultContract.gapCodes.length === 0 && receipt.resultContract.blockers.length === 0 && availability.available === false && !availability.currentOfferVersionRef && !availability.verifiedOfferRef && !availability.gapCodes?.length;
128
+ const completeNegative = (receipt.status === 'unavailable' || receipt.status === 'no_match')
129
+ && receipt.resultContract.outcome === 'empty'
130
+ && receipt.resultContract.gapCodes.length === 0
131
+ && receipt.resultContract.blockers.length === 0
132
+ && availability.available === false
133
+ && !availability.currentOfferVersionRef
134
+ && !availability.verifiedOfferRef
135
+ && !availability.gapCodes?.length;
124
136
  if (completeNegative) {
125
- if (!hotel.tombstonedOfferVersionRefs.includes(action.input.offerVersionRef)) hotel.tombstonedOfferVersionRefs.push(action.input.offerVersionRef);
126
- if (!hotel.tombstonedOfferRefs.includes(action.input.offerRef)) hotel.tombstonedOfferRefs.push(action.input.offerRef);
137
+ if (!hotel.tombstonedOfferVersionRefs.includes(action.input.offerVersionRef))
138
+ hotel.tombstonedOfferVersionRefs.push(action.input.offerVersionRef);
139
+ if (!hotel.tombstonedOfferRefs.includes(action.input.offerRef))
140
+ hotel.tombstonedOfferRefs.push(action.input.offerRef);
127
141
  }
128
142
  }
129
143
  }
130
144
  return normalized;
131
145
  }
132
146
  function normalizeActionCheckpointForReplay(raw, workspace) {
133
- if (![
134
- 'offer.select',
135
- 'offer.check',
136
- 'checkout.prepare'
137
- ].includes(raw.kind) || typeof raw.input.offerVersionRef === 'string') return raw;
138
- if (typeof raw.input.offerRef !== 'string') throw new Error('booking_replan_required:offer_version_unbound');
147
+ if (!['offer.select', 'offer.check', 'checkout.prepare'].includes(raw.kind) || typeof raw.input.offerVersionRef === 'string')
148
+ return raw;
149
+ if (typeof raw.input.offerRef !== 'string')
150
+ throw new Error('booking_replan_required:offer_version_unbound');
139
151
  const offerVersionRef = resolveReplayOfferVersion(raw.input.offerRef, workspace, undefined, raw.actionId);
140
- const input = {
141
- ...raw.input,
142
- offerVersionRef
143
- };
144
- if (raw.kind === 'checkout.prepare' && (!workspace?.verifiedOffer || workspace.verifiedOffer.offerRef !== input.offerRef || workspace.verifiedOffer.offerVersionRef !== offerVersionRef || workspace.verifiedOffer.verifiedOfferRef !== input.verifiedOfferRef)) throw new Error('booking_replan_required:legacy_checkout_unverified');
152
+ const input = { ...raw.input, offerVersionRef };
153
+ if (raw.kind === 'checkout.prepare' && (!workspace?.verifiedOffer || workspace.verifiedOffer.offerRef !== input.offerRef || workspace.verifiedOffer.offerVersionRef !== offerVersionRef || workspace.verifiedOffer.verifiedOfferRef !== input.verifiedOfferRef))
154
+ throw new Error('booking_replan_required:legacy_checkout_unverified');
145
155
  const { actionDigest: _rawActionDigest, ...rawBase } = raw;
146
- const normalizedBase = {
147
- ...rawBase,
148
- input,
149
- inputDigest: bookingDigest(input)
150
- };
151
- return {
152
- ...normalizedBase,
153
- actionDigest: checkpointDigest(normalizedBase)
154
- };
156
+ const normalizedBase = { ...rawBase, input, inputDigest: bookingDigest(input) };
157
+ return { ...normalizedBase, actionDigest: checkpointDigest(normalizedBase) };
155
158
  }
156
159
  function checkpointDigestMatchesForReplay(raw, normalized) {
157
160
  const { actionDigest: _rawDigest, ...rawBase } = raw;
@@ -159,11 +162,8 @@ function checkpointDigestMatchesForReplay(raw, normalized) {
159
162
  return raw.actionDigest === checkpointDigest(rawBase) || raw.actionDigest === checkpointDigest(normalizedBase);
160
163
  }
161
164
  function replanAfterReplayUpgradeGap(state, workspace = state.workspaceSnapshot, revision = workspace?.revision) {
162
- if (!workspace) return {
163
- ...state,
164
- phase: 'error',
165
- pendingAction: undefined
166
- };
165
+ if (!workspace)
166
+ return { ...state, phase: 'error', pendingAction: undefined };
167
167
  const safeRevision = typeof revision === 'number' && Number.isSafeInteger(revision) ? revision : state.revision;
168
168
  return {
169
169
  ...state,
@@ -175,15 +175,14 @@ function replanAfterReplayUpgradeGap(state, workspace = state.workspaceSnapshot,
175
175
  workspaceSemanticDigest: bookingWorkspaceSemanticDigest(workspace),
176
176
  workspaceSnapshot: workspace,
177
177
  availability: createAvailabilityPolicy(workspace),
178
- replayUpgradeRequired: true
178
+ replayUpgradeRequired: true,
179
179
  };
180
180
  }
181
181
  function assertReplayUpgradeReanchored(state) {
182
- if (state.replayUpgradeRequired) throw new Error('reanchor_turn_required');
183
- }
184
- function consumedIdentityKey(eventId, sequence) {
185
- return `${eventId}\0${sequence}`;
182
+ if (state.replayUpgradeRequired)
183
+ throw new Error('reanchor_turn_required');
186
184
  }
185
+ function consumedIdentityKey(eventId, sequence) { return `${eventId}\0${sequence}`; }
187
186
  function clearPendingBatch(consumed) {
188
187
  consumed.pendingBatchKeys.clear();
189
188
  consumed.pendingBatchOpen = false;
@@ -208,9 +207,11 @@ function rememberConsumedActionIdentity(consumed, action) {
208
207
  function consumeSkippedActionOrdinal(state, payload, consumed) {
209
208
  const action = payload.action;
210
209
  const operationCount = payload.operationCount ?? state.operationCount + 1;
211
- if (!action || typeof action !== 'object') throw new Error('skip_action_invalid');
210
+ if (!action || typeof action !== 'object')
211
+ throw new Error('skip_action_invalid');
212
212
  const { actionDigest: _actionDigest, ...actionBase } = action;
213
- if (action.actionDigest !== checkpointDigest(actionBase) || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount + 1 || operationCount > BOOKING_COPILOT_MAX_OPERATIONS || !Number.isSafeInteger(action.sequence) || action.sequence <= state.lastSequence) throw new Error('skip_action_invalid');
213
+ if (action.actionDigest !== checkpointDigest(actionBase) || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount + 1 || operationCount > BOOKING_COPILOT_MAX_OPERATIONS || !Number.isSafeInteger(action.sequence) || action.sequence <= state.lastSequence)
214
+ throw new Error('skip_action_invalid');
214
215
  state.operationCount = operationCount;
215
216
  state.lastSequence = action.sequence;
216
217
  if (consumed) {
@@ -218,94 +219,92 @@ function consumeSkippedActionOrdinal(state, payload, consumed) {
218
219
  }
219
220
  }
220
221
  function eventDraftDigest(event) {
221
- if (event.kind === 'status') return bookingDigest({
222
- kind: event.kind,
223
- status: event.status
224
- });
225
- if (event.kind === 'question') return bookingDigest({
226
- kind: event.kind,
227
- question: event.question
228
- });
229
- if (event.kind === 'explanation') return bookingDigest({
230
- kind: event.kind,
231
- explanation: event.explanation
232
- });
233
- if (event.kind === 'terminal') return bookingDigest({
234
- kind: event.kind,
235
- terminal: event.terminal
236
- });
237
- return bookingDigest({
238
- kind: event.kind,
239
- error: event.error
240
- });
222
+ if (event.kind === 'status')
223
+ return bookingDigest({ kind: event.kind, status: event.status });
224
+ if (event.kind === 'question')
225
+ return bookingDigest({ kind: event.kind, question: event.question });
226
+ if (event.kind === 'explanation')
227
+ return bookingDigest({ kind: event.kind, explanation: event.explanation });
228
+ if (event.kind === 'terminal')
229
+ return bookingDigest({ kind: event.kind, terminal: event.terminal });
230
+ return bookingDigest({ kind: event.kind, error: event.error });
241
231
  }
242
232
  function eventPayloadMatchesEvent(payload, event) {
243
233
  const expectedDigest = eventDraftDigest(event);
244
234
  const legacyTerminalDigest = event.kind === 'terminal' ? bookingDigest(event.terminal) : undefined;
245
- return payload.eventId === event.eventId && payload.sequence === event.sequence && payload.emittedAt === event.emittedAt && payload.contextRef === event.contextRef && payload.eventKind === event.kind && (event.kind !== 'status' || payload.status === event.status) && (payload.contentDigest === expectedDigest || payload.contentDigest === legacyTerminalDigest);
235
+ return payload.eventId === event.eventId
236
+ && payload.sequence === event.sequence
237
+ && payload.emittedAt === event.emittedAt
238
+ && payload.contextRef === event.contextRef
239
+ && payload.eventKind === event.kind
240
+ && (event.kind !== 'status' || payload.status === event.status)
241
+ && (payload.contentDigest === expectedDigest || payload.contentDigest === legacyTerminalDigest);
246
242
  }
247
243
  function actionFromOperationEventForReplay(event, checkpoint) {
248
244
  const action = event.action;
249
245
  if ((action.kind === 'offer.select' || action.kind === 'offer.check' || action.kind === 'checkout.prepare') && typeof action.input.offerRef === 'string' && typeof action.input.offerVersionRef !== 'string' && typeof checkpoint.input.offerVersionRef === 'string') {
250
- return {
251
- ...action,
252
- input: {
253
- ...action.input,
254
- offerVersionRef: checkpoint.input.offerVersionRef
255
- }
256
- };
246
+ return { ...action, input: { ...action.input, offerVersionRef: checkpoint.input.offerVersionRef } };
257
247
  }
258
248
  return action;
259
249
  }
260
250
  function operationEventMatchesAction(event, checkpoint) {
261
- return event.eventId === checkpoint.eventId && event.sequence === checkpoint.sequence && event.emittedAt === checkpoint.emittedAt && event.contextRef === checkpoint.contextRef && matchesCheckpoint(actionFromOperationEventForReplay(event, checkpoint), checkpoint);
251
+ return event.eventId === checkpoint.eventId
252
+ && event.sequence === checkpoint.sequence
253
+ && event.emittedAt === checkpoint.emittedAt
254
+ && event.contextRef === checkpoint.contextRef
255
+ && matchesCheckpoint(actionFromOperationEventForReplay(event, checkpoint), checkpoint);
262
256
  }
263
257
  function normalizeDecisionBatchEnvelopeForReplay(payload, consumed, requireSchemaValid = false) {
264
- if (typeof payload.requestKey !== 'string' || !payload.requestKey || !payload.events.length) throw new Error('skip_decision_batch_invalid');
258
+ if (typeof payload.requestKey !== 'string' || !payload.requestKey || !payload.events.length)
259
+ throw new Error('skip_decision_batch_invalid');
265
260
  let lastBatchSequence = -1;
266
261
  const seen = new Set();
267
262
  const normalizedEvents = [];
268
- for (const event of payload.events){
269
- if (!Number.isSafeInteger(event.sequence) || event.sequence <= lastBatchSequence) throw new Error('skip_decision_batch_invalid');
263
+ for (const event of payload.events) {
264
+ if (!Number.isSafeInteger(event.sequence) || event.sequence <= lastBatchSequence)
265
+ throw new Error('skip_decision_batch_invalid');
270
266
  lastBatchSequence = event.sequence;
271
267
  const key = consumedIdentityKey(event.eventId, event.sequence);
272
- if (seen.has(key)) throw new Error('skip_decision_batch_invalid');
268
+ if (seen.has(key))
269
+ throw new Error('skip_decision_batch_invalid');
273
270
  seen.add(key);
274
271
  if (event.kind === 'operation') {
275
272
  const checkpoint = consumed.actions.get(key);
276
- if (!checkpoint || !operationEventMatchesAction(event, checkpoint)) throw new Error('skip_decision_batch_invalid');
277
- const normalized = {
278
- ...event,
279
- action: actionFromOperationEventForReplay(event, checkpoint)
280
- };
281
- if (requireSchemaValid && !validateBookingSurfaceEvent(normalized).ok) throw new Error('skip_decision_batch_invalid');
273
+ if (!checkpoint || !operationEventMatchesAction(event, checkpoint))
274
+ throw new Error('skip_decision_batch_invalid');
275
+ const normalized = { ...event, action: actionFromOperationEventForReplay(event, checkpoint) };
276
+ if (requireSchemaValid && !validateBookingSurfaceEvent(normalized).ok)
277
+ throw new Error('skip_decision_batch_invalid');
282
278
  normalizedEvents.push(normalized);
283
- } else {
279
+ }
280
+ else {
284
281
  const recorded = consumed.events.get(key);
285
- if (!recorded || !validateBookingSurfaceEvent(event).ok || !eventPayloadMatchesEvent(recorded, event)) throw new Error('skip_decision_batch_invalid');
282
+ if (!recorded || !validateBookingSurfaceEvent(event).ok || !eventPayloadMatchesEvent(recorded, event))
283
+ throw new Error('skip_decision_batch_invalid');
286
284
  normalizedEvents.push(event);
287
285
  }
288
- if (!consumed.pendingBatchKeys.has(key)) throw new Error('skip_decision_batch_invalid');
286
+ if (!consumed.pendingBatchKeys.has(key))
287
+ throw new Error('skip_decision_batch_invalid');
289
288
  }
290
- if (seen.size !== consumed.pendingBatchKeys.size) throw new Error('skip_decision_batch_invalid');
291
- for (const key of consumed.pendingBatchKeys)if (!seen.has(key)) throw new Error('skip_decision_batch_invalid');
289
+ if (seen.size !== consumed.pendingBatchKeys.size)
290
+ throw new Error('skip_decision_batch_invalid');
291
+ for (const key of consumed.pendingBatchKeys)
292
+ if (!seen.has(key))
293
+ throw new Error('skip_decision_batch_invalid');
292
294
  clearPendingBatch(consumed);
293
295
  return normalizedEvents;
294
296
  }
295
297
  function consumeSkippedEventSequence(state, payload, consumed) {
296
- if (!payload || typeof payload !== 'object') throw new Error('skip_event_invalid');
298
+ if (!payload || typeof payload !== 'object')
299
+ throw new Error('skip_event_invalid');
297
300
  if ('sequence' in payload) {
298
301
  const eventPayload = payload;
299
- if (typeof eventPayload.eventId !== 'string' || !eventPayload.eventId || ![
300
- 'status',
301
- 'question',
302
- 'explanation',
303
- 'terminal',
304
- 'error'
305
- ].includes(String(eventPayload.eventKind)) || typeof eventPayload.contentDigest !== 'string' || !/^[0-9a-f]{64}$/.test(eventPayload.contentDigest) || typeof eventPayload.sequence !== 'number' || !Number.isSafeInteger(eventPayload.sequence) || eventPayload.sequence <= state.lastSequence) throw new Error('skip_event_invalid');
302
+ if (typeof eventPayload.eventId !== 'string' || !eventPayload.eventId || !['status', 'question', 'explanation', 'terminal', 'error'].includes(String(eventPayload.eventKind)) || typeof eventPayload.contentDigest !== 'string' || !/^[0-9a-f]{64}$/.test(eventPayload.contentDigest) || typeof eventPayload.sequence !== 'number' || !Number.isSafeInteger(eventPayload.sequence) || eventPayload.sequence <= state.lastSequence)
303
+ throw new Error('skip_event_invalid');
306
304
  state.lastSequence = eventPayload.sequence;
307
305
  rememberConsumedEventIdentity(consumed, eventPayload);
308
- } else if ('events' in payload && Array.isArray(payload.events)) {
306
+ }
307
+ else if ('events' in payload && Array.isArray(payload.events)) {
309
308
  normalizeDecisionBatchEnvelopeForReplay(payload, consumed);
310
309
  }
311
310
  }
@@ -315,76 +314,81 @@ function normalizeReceiptForReplay(raw, action, workspace) {
315
314
  receipt.observation.offerVersionRef = action.input.offerVersionRef;
316
315
  }
317
316
  if (action.kind === 'checkout.prepare' && receipt.observation.kind === 'checkout.handoff' && receipt.observation.offerRef === action.input.offerRef && !receipt.observation.offerVersionRef) {
318
- if (!workspace.verifiedOffer || workspace.verifiedOffer.offerRef !== action.input.offerRef || workspace.verifiedOffer.offerVersionRef !== action.input.offerVersionRef || workspace.verifiedOffer.verifiedOfferRef !== action.input.verifiedOfferRef) throw new Error('booking_replan_required:legacy_checkout_unverified');
317
+ if (!workspace.verifiedOffer || workspace.verifiedOffer.offerRef !== action.input.offerRef || workspace.verifiedOffer.offerVersionRef !== action.input.offerVersionRef || workspace.verifiedOffer.verifiedOfferRef !== action.input.verifiedOfferRef)
318
+ throw new Error('booking_replan_required:legacy_checkout_unverified');
319
319
  receipt.observation.offerVersionRef = action.input.offerVersionRef;
320
320
  }
321
321
  if (action.kind === 'offer.check' && receipt.observation.kind === 'offer.availability' && receipt.observation.offerRef === action.input.offerRef) {
322
- if (!receipt.observation.checkedOfferVersionRef) receipt.observation.checkedOfferVersionRef = action.input.offerVersionRef;
323
- if (receipt.status === 'applied' && receipt.observation.available === true && !receipt.observation.currentOfferVersionRef) receipt.observation.currentOfferVersionRef = action.input.offerVersionRef;
322
+ if (!receipt.observation.checkedOfferVersionRef)
323
+ receipt.observation.checkedOfferVersionRef = action.input.offerVersionRef;
324
+ if (receipt.status === 'applied' && receipt.observation.available === true && !receipt.observation.currentOfferVersionRef)
325
+ receipt.observation.currentOfferVersionRef = action.input.offerVersionRef;
324
326
  if (receipt.status === 'changed' && receipt.observation.available === true && !receipt.observation.currentOfferVersionRef) {
325
- const current = workspace.loadedOffers.find((offer)=>offer.offerRef === action.input.offerRef && offer.offerVersionRef !== action.input.offerVersionRef);
326
- if (!current) throw new Error('booking_replan_required:changed_version_unbound');
327
+ const current = workspace.loadedOffers.find((offer) => offer.offerRef === action.input.offerRef && offer.offerVersionRef !== action.input.offerVersionRef);
328
+ if (!current)
329
+ throw new Error('booking_replan_required:changed_version_unbound');
327
330
  receipt.observation.currentOfferVersionRef = current.offerVersionRef;
328
331
  }
329
- if (receipt.status === 'applied' && receipt.observation.verifiedOfferRef && (!workspace.verifiedOffer || workspace.verifiedOffer.offerRef !== action.input.offerRef || workspace.verifiedOffer.offerVersionRef !== action.input.offerVersionRef || workspace.verifiedOffer.verifiedOfferRef !== receipt.observation.verifiedOfferRef)) throw new Error('booking_replan_required:legacy_verified_scalar');
332
+ if (receipt.status === 'applied' && receipt.observation.verifiedOfferRef && (!workspace.verifiedOffer || workspace.verifiedOffer.offerRef !== action.input.offerRef || workspace.verifiedOffer.offerVersionRef !== action.input.offerVersionRef || workspace.verifiedOffer.verifiedOfferRef !== receipt.observation.verifiedOfferRef))
333
+ throw new Error('booking_replan_required:legacy_verified_scalar');
330
334
  }
331
- if (receipt.resultContract.blockers.length && bookingDigest(raw) !== bookingDigest(receipt)) throw new Error('booking_replan_required:legacy_blocker_digest');
335
+ if (receipt.resultContract.blockers.length && bookingDigest(raw) !== bookingDigest(receipt))
336
+ throw new Error('booking_replan_required:legacy_blocker_digest');
332
337
  return receipt;
333
338
  }
334
- function errorText(result) {
335
- return result.ok ? '' : result.errors.join('; ');
336
- }
337
- function assertTaskId(taskId) {
338
- if (!/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(taskId)) throw new Error('invalid_task_id');
339
- }
340
- function assertSafeRef(value) {
341
- if (!/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(value)) throw new Error('unsafe_opaque_ref');
342
- }
343
- function safeIdentity(value) {
344
- return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(value);
345
- }
346
- function sameActions(a, b) {
347
- return a.length === b.length && [
348
- ...a
349
- ].sort().every((x, i)=>x === [
350
- ...b
351
- ].sort()[i]);
352
- }
353
- function availabilityPolicyTerminal(state) {
354
- return availabilityPolicyResult(state);
355
- }
339
+ function errorText(result) { return result.ok ? '' : result.errors.join('; '); }
340
+ function assertTaskId(taskId) { if (!/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(taskId))
341
+ throw new Error('invalid_task_id'); }
342
+ function assertSafeRef(value) { if (!/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(value))
343
+ throw new Error('unsafe_opaque_ref'); }
344
+ function safeIdentity(value) { return typeof value === 'string' && /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(value); }
345
+ function sameActions(a, b) { return a.length === b.length && [...a].sort().every((x, i) => x === [...b].sort()[i]); }
346
+ function availabilityPolicyTerminal(state) { return availabilityPolicyResult(state); }
356
347
  function taskHasTerminalBudget(state) {
357
348
  return state.operationCount >= BOOKING_COPILOT_MAX_OPERATIONS || Boolean(state.availability.terminal);
358
349
  }
359
350
  function assertRequestKey(requestKey) {
360
- if (!isSafeBookingRequestKey(requestKey)) throw new Error('unsafe_request_key');
351
+ if (!isSafeBookingRequestKey(requestKey))
352
+ throw new Error('unsafe_request_key');
361
353
  }
354
+ /** Decision batches are runtime-owned and may contain long task/turn/action refs. */
362
355
  function assertInternalDecisionKey(requestKey) {
363
- if (typeof requestKey !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,2047}$/.test(requestKey)) throw new Error('unsafe_internal_decision_key');
356
+ if (typeof requestKey !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,2047}$/.test(requestKey))
357
+ throw new Error('unsafe_internal_decision_key');
364
358
  }
365
359
  function assertDecisionBatchFinality(decisions) {
366
360
  let finalSeen = false;
367
- for (const decision of decisions){
361
+ for (const decision of decisions) {
368
362
  const isFinal = decision.kind === 'terminal' || decision.kind === 'error';
369
- if (finalSeen) throw new Error('decision_batch_finality');
370
- if (isFinal) finalSeen = true;
363
+ if (finalSeen)
364
+ throw new Error('decision_batch_finality');
365
+ if (isFinal)
366
+ finalSeen = true;
371
367
  }
372
368
  }
373
369
  function assertPrincipal(principal) {
374
- if (!principal || typeof principal.subject !== 'string' || !principal.subject || principal.subject.length > 256 || typeof principal.scope !== 'string' || !principal.scope || principal.scope.length > 256) throw new Error('trusted_principal_required');
370
+ if (!principal || typeof principal.subject !== 'string' || !principal.subject || principal.subject.length > 256 || typeof principal.scope !== 'string' || !principal.scope || principal.scope.length > 256)
371
+ throw new Error('trusted_principal_required');
375
372
  }
376
373
  function assertAvailabilityLiveness(state, decisions) {
377
- if (!state.availability.recoveryStarted || state.awaitingApproval) return;
378
- const operations = decisions.filter((decision)=>decision.kind === 'operation');
379
- if (operations.length !== 1 || !state.workspaceSnapshot) throw new Error('availability_operation_required');
374
+ if (!state.availability.recoveryStarted || state.awaitingApproval)
375
+ return;
376
+ const operations = decisions.filter((decision) => decision.kind === 'operation');
377
+ if (operations.length !== 1 || !state.workspaceSnapshot)
378
+ throw new Error('availability_operation_required');
380
379
  const action = operations[0].action;
381
380
  const activeHotelRef = state.availability.hotelRefs[state.availability.activeHotelOrdinal];
382
- if (!activeHotelRef) throw new Error('availability_active_hotel_missing');
381
+ if (!activeHotelRef)
382
+ throw new Error('availability_active_hotel_missing');
383
383
  if (state.availability.availabilityPhase === 'need_offers') {
384
- if (action.kind !== 'offers.query' || action.input.hotelRefs.length !== 1 || action.input.hotelRefs[0] !== activeHotelRef) throw new Error('availability_operation_incompatible');
385
- } else if (state.availability.availabilityPhase === 'need_check') {
386
- if (action.kind !== 'offer.check' || action.input.offerRef === undefined || action.input.offerVersionRef === undefined || !state.availability.hotels[activeHotelRef]?.currentOfferRefs.includes(action.input.offerRef) || !canIssueOfferCheck(state.availability, state.workspaceSnapshot, action.input.offerRef, action.input.offerVersionRef).ok) throw new Error('availability_operation_incompatible');
387
- } else {
384
+ if (action.kind !== 'offers.query' || action.input.hotelRefs.length !== 1 || action.input.hotelRefs[0] !== activeHotelRef)
385
+ throw new Error('availability_operation_incompatible');
386
+ }
387
+ else if (state.availability.availabilityPhase === 'need_check') {
388
+ if (action.kind !== 'offer.check' || action.input.offerRef === undefined || action.input.offerVersionRef === undefined || !state.availability.hotels[activeHotelRef]?.currentOfferRefs.includes(action.input.offerRef) || !canIssueOfferCheck(state.availability, state.workspaceSnapshot, action.input.offerRef, action.input.offerVersionRef).ok)
389
+ throw new Error('availability_operation_incompatible');
390
+ }
391
+ else {
388
392
  throw new Error('availability_operation_incompatible');
389
393
  }
390
394
  }
@@ -397,9 +401,7 @@ function checkpointFor(action, eventId, sequence, emittedAt, sourceTurnId) {
397
401
  kind: action.kind,
398
402
  contextRef: action.contextRef,
399
403
  expectedRevision: action.expectedRevision,
400
- factRefs: [
401
- ...action.factRefs
402
- ],
404
+ factRefs: [...action.factRefs],
403
405
  reasonDigest: bookingDigest(action.reason),
404
406
  inputDigest: bookingDigest(action.input),
405
407
  input: action.input,
@@ -407,20 +409,12 @@ function checkpointFor(action, eventId, sequence, emittedAt, sourceTurnId) {
407
409
  sequence,
408
410
  emittedAt,
409
411
  sourceTurnId,
410
- ...action.relaxationApprovalRef ? {
411
- relaxationApprovalRef: action.relaxationApprovalRef
412
- } : {}
413
- };
414
- return {
415
- ...base,
416
- actionDigest: checkpointDigest(base)
412
+ ...(action.relaxationApprovalRef ? { relaxationApprovalRef: action.relaxationApprovalRef } : {}),
417
413
  };
414
+ return { ...base, actionDigest: checkpointDigest(base) };
418
415
  }
419
416
  function matchesCheckpoint(action, checkpoint) {
420
- const candidate = checkpointFor(action.relaxationApprovalRef || !checkpoint.relaxationApprovalRef ? action : {
421
- ...action,
422
- relaxationApprovalRef: checkpoint.relaxationApprovalRef
423
- }, checkpoint.eventId, checkpoint.sequence, checkpoint.emittedAt, checkpoint.sourceTurnId);
417
+ const candidate = checkpointFor(action.relaxationApprovalRef || !checkpoint.relaxationApprovalRef ? action : { ...action, relaxationApprovalRef: checkpoint.relaxationApprovalRef }, checkpoint.eventId, checkpoint.sequence, checkpoint.emittedAt, checkpoint.sourceTurnId);
424
418
  const { actionDigest: _ignored, ...candidateBase } = candidate;
425
419
  return checkpoint.actionDigest === checkpointDigest(candidateBase);
426
420
  }
@@ -429,58 +423,57 @@ function receiptSourceDigest(receipt) {
429
423
  ...receipt,
430
424
  resultContract: {
431
425
  ...receipt.resultContract,
432
- blockers: receipt.resultContract.blockers.map(({ sourceReceiptDigest: _ignored, ...blocker })=>blocker)
433
- }
426
+ blockers: receipt.resultContract.blockers.map(({ sourceReceiptDigest: _ignored, ...blocker }) => blocker),
427
+ },
434
428
  });
435
429
  }
436
430
  function assertCanonicalReceipt(receipt) {
437
431
  const expected = receiptSourceDigest(receipt);
438
- if (receipt.resultContract.blockers.some((blocker)=>blocker.sourceReceiptDigest !== expected)) throw new Error('receipt_source_digest_mismatch');
432
+ if (receipt.resultContract.blockers.some((blocker) => blocker.sourceReceiptDigest !== expected))
433
+ throw new Error('receipt_source_digest_mismatch');
439
434
  }
440
435
  function receiptObservationMatchesAction(observation, action) {
441
- if (observation === 'gap') return true;
436
+ if (observation === 'gap')
437
+ return true;
442
438
  const expected = {
443
- 'search.patch': 'search.state',
444
- 'search.run': 'search.state',
445
- 'results.view.patch': 'results.state',
446
- 'hotel.focus': 'hotel.focus',
447
- 'hotel.select': 'hotel.selection',
448
- 'offers.query': 'offers.state',
449
- 'offers.view.patch': 'offers.state',
450
- 'offers.compare': 'offers.state',
451
- 'offer.select': 'offer.selection',
452
- 'offer.check': 'offer.availability',
453
- 'checkout.prepare': 'checkout.handoff',
454
- 'order.observe': 'order.state'
439
+ 'search.patch': 'search.state', 'search.run': 'search.state', 'results.view.patch': 'results.state',
440
+ 'hotel.focus': 'hotel.focus', 'hotel.select': 'hotel.selection', 'offers.query': 'offers.state',
441
+ 'offers.view.patch': 'offers.state', 'offers.compare': 'offers.state', 'offer.select': 'offer.selection',
442
+ 'offer.check': 'offer.availability', 'checkout.prepare': 'checkout.handoff', 'order.observe': 'order.state',
455
443
  };
456
444
  return expected[action] === observation;
457
445
  }
458
446
  function sameRefSet(a, b) {
459
- return a.length === b.length && [
460
- ...a
461
- ].sort().every((value, index)=>value === [
462
- ...b
463
- ].sort()[index]);
447
+ return a.length === b.length && [...a].sort().every((value, index) => value === [...b].sort()[index]);
464
448
  }
465
449
  function receiptTargetMatchesAction(receipt, action) {
466
450
  const observation = receipt.observation;
467
451
  const input = action.input;
468
- if (action.kind === 'hotel.focus' && observation.kind === 'hotel.focus') return observation.hotelRef === input.hotelRef;
469
- if (action.kind === 'hotel.select' && observation.kind === 'hotel.selection') return observation.hotelRef === input.hotelRef;
470
- if (action.kind === 'offer.select' && observation.kind === 'offer.selection') return observation.offerRef === input.offerRef && observation.offerVersionRef === input.offerVersionRef;
471
- if (action.kind === 'offer.check' && observation.kind === 'offer.availability') return observation.offerRef === input.offerRef && observation.checkedOfferVersionRef === input.offerVersionRef;
472
- if (action.kind === 'checkout.prepare' && observation.kind === 'checkout.handoff') return observation.offerRef === input.offerRef && observation.offerVersionRef === input.offerVersionRef && observation.verifiedOfferRef === input.verifiedOfferRef;
473
- if (action.kind === 'offers.query' && observation.kind === 'offers.state') return sameRefSet(observation.hotelRefs, input.hotelRefs);
474
- if (action.kind === 'offers.view.patch' && observation.kind === 'offers.state') return observation.hotelRefs.includes(input.hotelRef);
475
- if (action.kind === 'offers.compare' && observation.kind === 'offers.state') return input.offerRefs.every((offerRef)=>observation.offerRefs.includes(offerRef));
476
- if (action.kind === 'order.observe' && observation.kind === 'order.state') return observation.orderRef === input.orderRef;
452
+ if (action.kind === 'hotel.focus' && observation.kind === 'hotel.focus')
453
+ return observation.hotelRef === input.hotelRef;
454
+ if (action.kind === 'hotel.select' && observation.kind === 'hotel.selection')
455
+ return observation.hotelRef === input.hotelRef;
456
+ if (action.kind === 'offer.select' && observation.kind === 'offer.selection')
457
+ return observation.offerRef === input.offerRef && observation.offerVersionRef === input.offerVersionRef;
458
+ if (action.kind === 'offer.check' && observation.kind === 'offer.availability')
459
+ return observation.offerRef === input.offerRef && observation.checkedOfferVersionRef === input.offerVersionRef;
460
+ if (action.kind === 'checkout.prepare' && observation.kind === 'checkout.handoff')
461
+ return observation.offerRef === input.offerRef && observation.offerVersionRef === input.offerVersionRef && observation.verifiedOfferRef === input.verifiedOfferRef;
462
+ if (action.kind === 'offers.query' && observation.kind === 'offers.state')
463
+ return sameRefSet(observation.hotelRefs, input.hotelRefs);
464
+ if (action.kind === 'offers.view.patch' && observation.kind === 'offers.state')
465
+ return observation.hotelRefs.includes(input.hotelRef);
466
+ if (action.kind === 'offers.compare' && observation.kind === 'offers.state')
467
+ return input.offerRefs.every((offerRef) => observation.offerRefs.includes(offerRef));
468
+ if (action.kind === 'order.observe' && observation.kind === 'order.state')
469
+ return observation.orderRef === input.orderRef;
477
470
  return observation.kind === 'gap' || action.kind === 'search.patch' || action.kind === 'search.run' || action.kind === 'results.view.patch';
478
471
  }
479
472
  function workspaceBoundaryMatches(a, b) {
480
473
  return a.locale === b.locale && a.currency === b.currency && a.surface === b.surface && a.contextRef === b.contextRef && a.capabilities.surface === b.capabilities.surface && sameActions(a.capabilities.allowedActions, b.capabilities.allowedActions);
481
474
  }
482
475
  function workspaceHasOfferVersion(workspace, offerRef, offerVersionRef) {
483
- return workspace.loadedOffers.some((offer)=>offer.offerRef === offerRef && offer.offerVersionRef === offerVersionRef);
476
+ return workspace.loadedOffers.some((offer) => offer.offerRef === offerRef && offer.offerVersionRef === offerVersionRef);
484
477
  }
485
478
  function verifiedOfferMatches(workspace, input) {
486
479
  return workspace.verifiedOffer?.offerRef === input.offerRef && workspace.verifiedOffer.offerVersionRef === input.offerVersionRef && workspace.verifiedOffer.verifiedOfferRef === input.verifiedOfferRef;
@@ -489,171 +482,142 @@ function verifiedOfferUnexpired(workspace, now) {
489
482
  return Boolean(workspace.verifiedOffer && isBookingDateTime(workspace.verifiedOffer.expiresAt) && Date.parse(workspace.verifiedOffer.expiresAt) > Date.parse(now));
490
483
  }
491
484
  function actionHitsCurrentOfferVersion(workspace, action, now) {
492
- if (action.kind === 'offer.select' || action.kind === 'offer.check') return workspaceHasOfferVersion(workspace, action.input.offerRef, action.input.offerVersionRef);
493
- if (action.kind === 'checkout.prepare') return workspaceHasOfferVersion(workspace, action.input.offerRef, action.input.offerVersionRef) && verifiedOfferMatches(workspace, action.input) && (now === undefined || verifiedOfferUnexpired(workspace, now));
485
+ if (action.kind === 'offer.select' || action.kind === 'offer.check')
486
+ return workspaceHasOfferVersion(workspace, action.input.offerRef, action.input.offerVersionRef);
487
+ if (action.kind === 'checkout.prepare')
488
+ return workspaceHasOfferVersion(workspace, action.input.offerRef, action.input.offerVersionRef) && verifiedOfferMatches(workspace, action.input) && (now === undefined || verifiedOfferUnexpired(workspace, now));
494
489
  return true;
495
490
  }
496
491
  function shortlistAfterCheckedOffer(previous, current, checkedOfferRef) {
497
- if (sameActions(previous, current)) return true;
498
- const removedChecked = previous.filter((ref)=>ref !== checkedOfferRef);
492
+ if (sameActions(previous, current))
493
+ return true;
494
+ const removedChecked = previous.filter((ref) => ref !== checkedOfferRef);
499
495
  return removedChecked.length === previous.length - 1 && sameActions(removedChecked, current);
500
496
  }
501
497
  function loadedOffersUnchanged(previous, current) {
502
- return sameActions(previous.loadedOffers.map((offer)=>bookingDigest(offer)), current.loadedOffers.map((offer)=>bookingDigest(offer)));
498
+ return sameActions(previous.loadedOffers.map((offer) => bookingDigest(offer)), current.loadedOffers.map((offer) => bookingDigest(offer)));
503
499
  }
504
500
  function changedFactRefsMatch(previousFactRefs, currentFactRefs, changedFactRefs) {
505
- if (!changedFactRefs.length) return false;
501
+ if (!changedFactRefs.length)
502
+ return false;
506
503
  const before = new Set(previousFactRefs);
507
504
  const after = new Set(currentFactRefs);
508
505
  const diff = [
509
- ...previousFactRefs.filter((ref)=>!after.has(ref)),
510
- ...currentFactRefs.filter((ref)=>!before.has(ref))
506
+ ...previousFactRefs.filter((ref) => !after.has(ref)),
507
+ ...currentFactRefs.filter((ref) => !before.has(ref)),
511
508
  ];
512
509
  return sameActions(diff, changedFactRefs);
513
510
  }
514
511
  function loadedOffersChangedVersionOnly(previous, current, offerRef, currentOfferVersionRef, changedFactRefs) {
515
- if (previous.loadedOffers.length !== current.loadedOffers.length) return false;
516
- const previousChecked = previous.loadedOffers.find((offer)=>offer.offerRef === offerRef);
517
- const currentChecked = current.loadedOffers.find((offer)=>offer.offerRef === offerRef);
518
- if (!previousChecked || !currentChecked || previousChecked.hotelRef !== currentChecked.hotelRef || currentChecked.offerVersionRef !== currentOfferVersionRef || previousChecked.offerVersionRef === currentChecked.offerVersionRef) return false;
519
- if (previousChecked.evidenceLevel !== currentChecked.evidenceLevel) return false;
520
- if (!changedFactRefsMatch(previousChecked.factRefs, currentChecked.factRefs, changedFactRefs)) return false;
521
- const previousOther = previous.loadedOffers.filter((offer)=>offer.offerRef !== offerRef).map((offer)=>bookingDigest(offer));
522
- const currentOther = current.loadedOffers.filter((offer)=>offer.offerRef !== offerRef).map((offer)=>bookingDigest(offer));
512
+ if (previous.loadedOffers.length !== current.loadedOffers.length)
513
+ return false;
514
+ const previousChecked = previous.loadedOffers.find((offer) => offer.offerRef === offerRef);
515
+ const currentChecked = current.loadedOffers.find((offer) => offer.offerRef === offerRef);
516
+ if (!previousChecked || !currentChecked || previousChecked.hotelRef !== currentChecked.hotelRef || currentChecked.offerVersionRef !== currentOfferVersionRef || previousChecked.offerVersionRef === currentChecked.offerVersionRef)
517
+ return false;
518
+ if (previousChecked.evidenceLevel !== currentChecked.evidenceLevel)
519
+ return false;
520
+ if (!changedFactRefsMatch(previousChecked.factRefs, currentChecked.factRefs, changedFactRefs))
521
+ return false;
522
+ const previousOther = previous.loadedOffers.filter((offer) => offer.offerRef !== offerRef).map((offer) => bookingDigest(offer));
523
+ const currentOther = current.loadedOffers.filter((offer) => offer.offerRef !== offerRef).map((offer) => bookingDigest(offer));
523
524
  return sameActions(previousOther, currentOther);
524
525
  }
525
526
  function availabilityReceiptIsComplete(receipt) {
526
- return receipt.resultContract.outcome === 'complete' && receipt.resultContract.hardCriteriaMet && receipt.resultContract.gapCodes.length === 0 && receipt.resultContract.blockers.length === 0;
527
+ return receipt.resultContract.outcome === 'complete'
528
+ && receipt.resultContract.hardCriteriaMet
529
+ && receipt.resultContract.gapCodes.length === 0
530
+ && receipt.resultContract.blockers.length === 0;
527
531
  }
528
532
  function offerCheckPostActionMatches(previous, current, action, receipt) {
529
533
  const checkedOfferRef = action.input.offerRef;
530
534
  const checkedOfferVersionRef = action.input.offerVersionRef;
531
535
  const observation = receipt.observation.kind === 'offer.availability' ? receipt.observation : undefined;
532
536
  const negative = receipt.status === 'unavailable' || receipt.status === 'no_match';
533
- if ([
534
- 'applied',
535
- 'changed',
536
- 'unavailable',
537
- 'no_match'
538
- ].includes(receipt.status) && !observation) return false;
537
+ if (['applied', 'changed', 'unavailable', 'no_match'].includes(receipt.status) && !observation)
538
+ return false;
539
539
  if (receipt.status === 'applied' && observation?.available && observation.verifiedOfferRef) {
540
- if (!availabilityReceiptIsComplete(receipt) || observation.gapCodes?.length) return false;
541
- if (!loadedOffersUnchanged(previous, current)) return false;
542
- if (observation.checkedOfferVersionRef !== checkedOfferVersionRef || observation.currentOfferVersionRef !== checkedOfferVersionRef) return false;
543
- if (current.selectedOfferRef !== checkedOfferRef) return false;
544
- if (!sameActions(previous.shortlistedOfferRefs, current.shortlistedOfferRefs)) return false;
545
- if (!current.verifiedOffer || current.verifiedOffer.offerRef !== checkedOfferRef || current.verifiedOffer.offerVersionRef !== checkedOfferVersionRef || current.verifiedOffer.verifiedOfferRef !== observation.verifiedOfferRef || Number.isNaN(Date.parse(current.verifiedOffer.expiresAt))) return false;
540
+ if (!availabilityReceiptIsComplete(receipt) || observation.gapCodes?.length)
541
+ return false;
542
+ if (!loadedOffersUnchanged(previous, current))
543
+ return false;
544
+ if (observation.checkedOfferVersionRef !== checkedOfferVersionRef || observation.currentOfferVersionRef !== checkedOfferVersionRef)
545
+ return false;
546
+ if (current.selectedOfferRef !== checkedOfferRef)
547
+ return false;
548
+ if (!sameActions(previous.shortlistedOfferRefs, current.shortlistedOfferRefs))
549
+ return false;
550
+ if (!current.verifiedOffer || current.verifiedOffer.offerRef !== checkedOfferRef || current.verifiedOffer.offerVersionRef !== checkedOfferVersionRef || current.verifiedOffer.verifiedOfferRef !== observation.verifiedOfferRef || Number.isNaN(Date.parse(current.verifiedOffer.expiresAt)))
551
+ return false;
546
552
  return true;
547
553
  }
548
554
  if (receipt.status === 'changed' && observation?.available && observation.currentOfferVersionRef) {
549
- if (observation.checkedOfferVersionRef !== checkedOfferVersionRef || observation.currentOfferVersionRef === checkedOfferVersionRef || observation.verifiedOfferRef || !observation.changedFactRefs.length) return false;
550
- if (!loadedOffersChangedVersionOnly(previous, current, checkedOfferRef, observation.currentOfferVersionRef, observation.changedFactRefs)) return false;
551
- if (current.selectedOfferRef !== checkedOfferRef || current.verifiedOffer !== undefined) return false;
555
+ if (observation.checkedOfferVersionRef !== checkedOfferVersionRef || observation.currentOfferVersionRef === checkedOfferVersionRef || observation.verifiedOfferRef || !observation.changedFactRefs.length)
556
+ return false;
557
+ if (!loadedOffersChangedVersionOnly(previous, current, checkedOfferRef, observation.currentOfferVersionRef, observation.changedFactRefs))
558
+ return false;
559
+ if (current.selectedOfferRef !== checkedOfferRef || current.verifiedOffer !== undefined)
560
+ return false;
552
561
  return sameActions(previous.shortlistedOfferRefs, current.shortlistedOfferRefs);
553
562
  }
554
- if (!loadedOffersUnchanged(previous, current)) return false;
563
+ if (!loadedOffersUnchanged(previous, current))
564
+ return false;
555
565
  if (negative) {
556
- if (current.selectedOfferRef !== undefined || current.verifiedOffer !== undefined) return false;
557
- if (!shortlistAfterCheckedOffer(previous.shortlistedOfferRefs, current.shortlistedOfferRefs, checkedOfferRef)) return false;
566
+ if (current.selectedOfferRef !== undefined || current.verifiedOffer !== undefined)
567
+ return false;
568
+ if (!shortlistAfterCheckedOffer(previous.shortlistedOfferRefs, current.shortlistedOfferRefs, checkedOfferRef))
569
+ return false;
558
570
  return !current.shortlistedOfferRefs.includes(checkedOfferRef);
559
571
  }
560
- if (!sameActions(previous.shortlistedOfferRefs, current.shortlistedOfferRefs)) return false;
561
- if (previous.selectedOfferRef !== current.selectedOfferRef) return false;
562
- if (bookingDigest(previous.verifiedOffer ?? null) !== bookingDigest(current.verifiedOffer ?? null)) return false;
572
+ if (!sameActions(previous.shortlistedOfferRefs, current.shortlistedOfferRefs))
573
+ return false;
574
+ if (previous.selectedOfferRef !== current.selectedOfferRef)
575
+ return false;
576
+ if (bookingDigest(previous.verifiedOffer ?? null) !== bookingDigest(current.verifiedOffer ?? null))
577
+ return false;
563
578
  return true;
564
579
  }
565
580
  function offerSelectPostActionMatches(previous, current, action, receipt) {
566
- if (receipt.status !== 'applied') return current.selectedOfferRef === previous.selectedOfferRef && loadedOffersUnchanged(previous, current);
567
- if (receipt.observation.kind !== 'offer.selection' || receipt.observation.offerRef !== action.input.offerRef || receipt.observation.offerVersionRef !== action.input.offerVersionRef) return false;
568
- if (current.selectedOfferRef !== action.input.offerRef) return false;
581
+ if (receipt.status !== 'applied')
582
+ return current.selectedOfferRef === previous.selectedOfferRef && loadedOffersUnchanged(previous, current);
583
+ if (receipt.observation.kind !== 'offer.selection' || receipt.observation.offerRef !== action.input.offerRef || receipt.observation.offerVersionRef !== action.input.offerVersionRef)
584
+ return false;
585
+ if (current.selectedOfferRef !== action.input.offerRef)
586
+ return false;
569
587
  return loadedOffersUnchanged(previous, current);
570
588
  }
571
589
  function workspacePostActionMatches(previous, current, action, receipt) {
572
- if (!workspaceBoundaryMatches(previous, current)) return false;
590
+ if (!workspaceBoundaryMatches(previous, current))
591
+ return false;
573
592
  const mutable = {
574
- 'search.patch': [
575
- 'searchDraft'
576
- ],
577
- 'search.run': [
578
- 'results',
579
- 'visibleHotels'
580
- ],
581
- 'results.view.patch': [
582
- 'results'
583
- ],
584
- 'hotel.focus': [
585
- 'focusedHotelRef'
586
- ],
587
- 'hotel.select': [
588
- 'focusedHotelRef'
589
- ],
590
- 'offers.query': [
591
- 'loadedOffers'
592
- ],
593
- 'offers.view.patch': [
594
- 'loadedOffers'
595
- ],
596
- 'offer.select': [
597
- 'selectedOfferRef'
598
- ],
599
- 'offer.check': [
600
- 'selectedOfferRef',
601
- 'shortlistedOfferRefs',
602
- 'verifiedOffer',
603
- 'loadedOffers'
604
- ]
593
+ 'search.patch': ['searchDraft'], 'search.run': ['results', 'visibleHotels'], 'results.view.patch': ['results'],
594
+ 'hotel.focus': ['focusedHotelRef'], 'hotel.select': ['focusedHotelRef'],
595
+ 'offers.query': ['loadedOffers'], 'offers.view.patch': ['loadedOffers'],
596
+ 'offer.select': ['selectedOfferRef'], 'offer.check': ['selectedOfferRef', 'shortlistedOfferRefs', 'verifiedOffer', 'loadedOffers'],
605
597
  };
606
- if (action.kind === 'offer.select' && !offerSelectPostActionMatches(previous, current, {
607
- kind: 'offer.select',
608
- input: {
609
- offerRef: action.input.offerRef,
610
- offerVersionRef: action.input.offerVersionRef
611
- }
612
- }, receipt)) return false;
613
- if (action.kind === 'offer.check' && !offerCheckPostActionMatches(previous, current, {
614
- kind: 'offer.check',
615
- input: {
616
- offerRef: action.input.offerRef,
617
- offerVersionRef: action.input.offerVersionRef
618
- }
619
- }, receipt)) return false;
620
- const allowed = new Set([
621
- 'revision',
622
- ...mutable[action.kind] ?? []
623
- ]);
624
- const keys = new Set([
625
- ...Object.keys(previous),
626
- ...Object.keys(current)
627
- ]);
628
- for (const key of keys){
629
- if (allowed.has(key)) continue;
630
- if (bookingDigest(previous[key]) !== bookingDigest(current[key])) return false;
598
+ if (action.kind === 'offer.select' && !offerSelectPostActionMatches(previous, current, { kind: 'offer.select', input: { offerRef: action.input.offerRef, offerVersionRef: action.input.offerVersionRef } }, receipt))
599
+ return false;
600
+ if (action.kind === 'offer.check' && !offerCheckPostActionMatches(previous, current, { kind: 'offer.check', input: { offerRef: action.input.offerRef, offerVersionRef: action.input.offerVersionRef } }, receipt))
601
+ return false;
602
+ const allowed = new Set(['revision', ...(mutable[action.kind] ?? [])]);
603
+ const keys = new Set([...Object.keys(previous), ...Object.keys(current)]);
604
+ for (const key of keys) {
605
+ if (allowed.has(key))
606
+ continue;
607
+ if (bookingDigest(previous[key]) !== bookingDigest(current[key]))
608
+ return false;
631
609
  }
632
610
  return true;
633
611
  }
634
612
  function approvalOption(taskId, contextRef, sourceTurnId, presentationRequestKey, blocker, to, deliveryNonce) {
635
613
  const base = {
636
- taskId,
637
- contextRef,
638
- sourceTurnId,
639
- presentationRequestKey,
640
- approvalId: `approval-${blocker.blockerId}-${to}`,
641
- deliveryNonce,
642
- blockerId: blocker.blockerId,
643
- sourceActionId: blocker.sourceActionId,
644
- sourceReceiptDigest: blocker.sourceReceiptDigest,
645
- scope: blocker.scope,
646
- code: blocker.code,
647
- criterionPath: blocker.criterionPath,
648
- valueDigest: blocker.valueDigest,
649
- from: 'must',
650
- to,
651
- approved: true
652
- };
653
- return {
654
- ...base,
655
- optionDigest: bookingDigest(base)
614
+ taskId, contextRef, sourceTurnId, presentationRequestKey,
615
+ approvalId: `approval-${blocker.blockerId}-${to}`, deliveryNonce, blockerId: blocker.blockerId,
616
+ sourceActionId: blocker.sourceActionId, sourceReceiptDigest: blocker.sourceReceiptDigest,
617
+ scope: blocker.scope, code: blocker.code, criterionPath: blocker.criterionPath,
618
+ valueDigest: blocker.valueDigest, from: 'must', to, approved: true,
656
619
  };
620
+ return { ...base, optionDigest: bookingDigest(base) };
657
621
  }
658
622
  function approvalOptionDigest(approval) {
659
623
  const { optionDigest: _optionDigest, ...base } = approval;
@@ -665,7 +629,8 @@ function presentationKey(taskId, sourceTurnId, actionId, receiptDigest) {
665
629
  function assertApprovalRef(ref, state, action) {
666
630
  const approval = state.awaitingApproval?.approval;
667
631
  const blocker = state.awaitingApproval?.blocker;
668
- if (!approval || !blocker || ref.approvalId !== approval.approvalId || ref.blockerId !== blocker.blockerId || ref.contextRef !== state.contextRef || ref.sourceTurnId !== state.awaitingApproval?.sourceTurnId || ref.presentationRequestKey !== state.awaitingApproval?.presentationRequestKey || ref.sourceActionId !== blocker.sourceActionId || ref.targetActionId !== action.actionId || ref.sourceRevision !== action.expectedRevision || ref.targetActionKind !== action.kind || ref.to !== approval.to || ref.expiresAt !== state.awaitingApproval?.expiresAt || ref.nonce !== state.awaitingApproval?.nonce || ref.sourceReceiptDigest !== blocker.sourceReceiptDigest || ref.scope !== blocker.scope || ref.code !== blocker.code || ref.criterionPath !== blocker.criterionPath || ref.valueDigest !== blocker.valueDigest) throw new Error('ledger_corrupt:approval_ref');
632
+ if (!approval || !blocker || ref.approvalId !== approval.approvalId || ref.blockerId !== blocker.blockerId || ref.contextRef !== state.contextRef || ref.sourceTurnId !== state.awaitingApproval?.sourceTurnId || ref.presentationRequestKey !== state.awaitingApproval?.presentationRequestKey || ref.sourceActionId !== blocker.sourceActionId || ref.targetActionId !== action.actionId || ref.sourceRevision !== action.expectedRevision || ref.targetActionKind !== action.kind || ref.to !== approval.to || ref.expiresAt !== state.awaitingApproval?.expiresAt || ref.nonce !== state.awaitingApproval?.nonce || ref.sourceReceiptDigest !== blocker.sourceReceiptDigest || ref.scope !== blocker.scope || ref.code !== blocker.code || ref.criterionPath !== blocker.criterionPath || ref.valueDigest !== blocker.valueDigest)
633
+ throw new Error('ledger_corrupt:approval_ref');
669
634
  }
670
635
  export class BookingCopilotTaskRuntime {
671
636
  ledger;
@@ -673,48 +638,60 @@ export class BookingCopilotTaskRuntime {
673
638
  contextRefFactory;
674
639
  now;
675
640
  approvalTtlMs;
676
- constructor(ledger, options = {}){
641
+ constructor(ledger, options = {}) {
677
642
  this.ledger = ledger;
678
- this.idFactory = options.idFactory ?? ((prefix)=>`${prefix}-${randomUUID()}`);
679
- this.contextRefFactory = options.contextRefFactory ?? (()=>this.idFactory('context'));
680
- this.now = options.now ?? (()=>new Date().toISOString());
643
+ this.idFactory = options.idFactory ?? ((prefix) => `${prefix}-${randomUUID()}`);
644
+ this.contextRefFactory = options.contextRefFactory ?? (() => this.idFactory('context'));
645
+ this.now = options.now ?? (() => new Date().toISOString());
681
646
  this.approvalTtlMs = options.approvalTtlMs ?? 120_000;
682
647
  }
648
+ /** Read-only conflict check used before startTask can append a new turn. */
683
649
  assertRequestBinding(requestKey, turn, input) {
684
650
  assertRequestKey(requestKey);
685
651
  assertPrincipal(input.principal);
686
- if (input.requestKey !== requestKey) throw new Error('request_conflict');
652
+ if (input.requestKey !== requestKey)
653
+ throw new Error('request_conflict');
687
654
  const existing = this.requestBinding(requestKey);
688
- if (!existing) return;
655
+ if (!existing)
656
+ return;
689
657
  const candidate = this.requestBindingPayload(requestKey, turn, input);
690
- if (existing.requestDigest !== candidate.requestDigest || existing.turnId !== candidate.turnId || existing.workspaceDigest !== candidate.workspaceDigest || existing.workspaceSemanticDigest !== candidate.workspaceSemanticDigest || existing.surface !== candidate.surface || existing.capabilityDigest !== candidate.capabilityDigest || existing.contextRef !== turn.workspace.contextRef || existing.principalDigest !== candidate.principalDigest || existing.scopeDigest !== candidate.scopeDigest || existing.taskHandle !== candidate.taskHandle) throw new Error('request_conflict');
658
+ if (existing.requestDigest !== candidate.requestDigest || existing.turnId !== candidate.turnId || existing.workspaceDigest !== candidate.workspaceDigest || existing.workspaceSemanticDigest !== candidate.workspaceSemanticDigest || existing.surface !== candidate.surface || existing.capabilityDigest !== candidate.capabilityDigest || existing.contextRef !== turn.workspace.contextRef || existing.principalDigest !== candidate.principalDigest || existing.scopeDigest !== candidate.scopeDigest || existing.taskHandle !== candidate.taskHandle)
659
+ throw new Error('request_conflict');
691
660
  }
661
+ /** Verifies a direct planner turn against its durable TURN row. */
692
662
  assertTurnBinding(taskId, turn) {
693
663
  assertTaskId(taskId);
694
664
  const row = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ? ORDER BY seq DESC`).all(this.ledger.tenant, taskId, TURN);
695
- const observed = row.map(({ payload })=>JSON.parse(payload)).find((candidate)=>candidate.turnId === turn.turnId);
696
- if (!observed || observed.requestDigest !== bookingTurnDigest(turn) || observed.workspaceDigest !== bookingWorkspaceDigest(turn.workspace) || observed.workspaceSemanticDigest !== bookingWorkspaceSemanticDigest(turn.workspace) || observed.contextRef !== turn.workspace.contextRef) throw new Error('turn_conflict');
665
+ const observed = row.map(({ payload }) => JSON.parse(payload)).find((candidate) => candidate.turnId === turn.turnId);
666
+ if (!observed || observed.requestDigest !== bookingTurnDigest(turn) || observed.workspaceDigest !== bookingWorkspaceDigest(turn.workspace) || observed.workspaceSemanticDigest !== bookingWorkspaceSemanticDigest(turn.workspace) || observed.contextRef !== turn.workspace.contextRef)
667
+ throw new Error('turn_conflict');
697
668
  }
669
+ /** Rejects a canonical approval after its durable runtime consumption. */
698
670
  assertApprovalNotConsumed(taskId, approval) {
699
671
  assertTaskId(taskId);
700
672
  const rows = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ?`).all(this.ledger.tenant, taskId, APPROVAL_CONSUMED);
701
- for (const row of rows){
673
+ for (const row of rows) {
702
674
  const consumed = JSON.parse(row.payload);
703
- if (consumed.approval?.approval && bookingDigest(consumed.approval.approval) === bookingDigest(approval)) throw new Error('approval_replayed');
675
+ if (consumed.approval?.approval && bookingDigest(consumed.approval.approval) === bookingDigest(approval))
676
+ throw new Error('approval_replayed');
704
677
  }
705
678
  }
679
+ /** Persists the per-requestKey receipt/replay binding after task creation. */
706
680
  persistRequestBinding(requestKey, turn, input) {
707
681
  assertRequestKey(requestKey);
708
682
  assertPrincipal(input.principal);
709
- if (input.requestKey !== requestKey) throw new Error('request_conflict');
710
- const run = this.ledger.db.transaction(()=>{
683
+ if (input.requestKey !== requestKey)
684
+ throw new Error('request_conflict');
685
+ const run = this.ledger.db.transaction(() => {
711
686
  const state = this.requireTask(turn.taskId);
712
- if (state.phase === 'terminal' || state.phase === 'error') throw new Error('task_terminal');
687
+ if (state.phase === 'terminal' || state.phase === 'error')
688
+ throw new Error('task_terminal');
713
689
  assertReplayUpgradeReanchored(state);
714
690
  this.appendRequestBindingInTransaction(requestKey, turn, input);
715
691
  });
716
692
  run.immediate();
717
693
  }
694
+ /** Binds the untrusted ingress snapshot to server-owned task/context/capabilities. */
718
695
  bindIngressTurn(turn, task, turnId) {
719
696
  assertSafeRef(turnId);
720
697
  const approval = turn.request.approval;
@@ -722,64 +699,57 @@ export class BookingCopilotTaskRuntime {
722
699
  throw new Error('invalid_ingress_approval_authority');
723
700
  }
724
701
  return {
725
- schemaVersion: 'booking.surface',
726
- kind: 'user.turn',
727
- taskId: task.taskId,
728
- turnId,
702
+ schemaVersion: 'booking.surface', kind: 'user.turn', taskId: task.taskId, turnId,
729
703
  workspace: {
730
704
  ...turn.workspace,
731
- schemaVersion: 'booking.surface',
732
- contextRef: task.contextRef,
733
- surface: task.surface,
734
- capabilities: {
735
- surface: task.surface,
736
- allowedActions: [
737
- ...task.allowedActions
738
- ]
739
- }
705
+ schemaVersion: 'booking.surface', contextRef: task.contextRef, surface: task.surface,
706
+ capabilities: { surface: task.surface, allowedActions: [...task.allowedActions] },
740
707
  },
741
- request: approval ? {
742
- text: turn.request.text,
743
- approval: {
744
- ...approval
745
- }
746
- } : {
747
- text: turn.request.text
748
- }
708
+ request: approval
709
+ ? { text: turn.request.text, approval: { ...approval } }
710
+ : { text: turn.request.text },
749
711
  };
750
712
  }
751
713
  startTask(turn, requestBinding) {
752
714
  const validation = validateBookingSurface(turn);
753
- if (!validation.ok) throw new Error(`invalid_planner_turn:${errorText(validation)}`);
754
- if (turn.kind === 'user.turn.ingress') throw new Error('ingress_binding_required');
755
- if (turn.kind === 'action.receipt.continuation') throw new Error('receipt_continuation_requires_continue');
715
+ if (!validation.ok)
716
+ throw new Error(`invalid_planner_turn:${errorText(validation)}`);
717
+ if (turn.kind === 'user.turn.ingress')
718
+ throw new Error('ingress_binding_required');
719
+ if (turn.kind === 'action.receipt.continuation')
720
+ throw new Error('receipt_continuation_requires_continue');
756
721
  const userTurn = turn;
757
- if (!userTurn.taskId) throw new Error('invalid_planner_turn:task_id_required');
722
+ if (!userTurn.taskId)
723
+ throw new Error('invalid_planner_turn:task_id_required');
758
724
  const taskId = userTurn.taskId;
759
725
  assertTaskId(taskId);
760
726
  const turnId = userTurn.turnId;
761
- if (!turnId) throw new Error('invalid_planner_turn:turn_id_required');
727
+ if (!turnId)
728
+ throw new Error('invalid_planner_turn:turn_id_required');
762
729
  assertSafeRef(turnId);
763
730
  const contextRef = userTurn.workspace.contextRef;
764
731
  const surface = userTurn.workspace.surface;
765
732
  const revision = userTurn.workspace.revision;
766
- const allowedActions = [
767
- ...userTurn.workspace.capabilities.allowedActions
768
- ];
733
+ const allowedActions = [...userTurn.workspace.capabilities.allowedActions];
769
734
  const requestDigest = bookingTurnDigest(turn);
770
735
  const boundWorkspace = userTurn.workspace;
771
736
  assertWorkspaceLoadedOfferRefsUnique(boundWorkspace);
772
737
  const workspaceDigest = bookingWorkspaceDigest(boundWorkspace);
773
738
  const workspaceSemanticDigest = bookingWorkspaceSemanticDigest(boundWorkspace);
774
739
  const availability = createAvailabilityPolicy(boundWorkspace);
775
- const run = this.ledger.db.transaction(()=>{
740
+ const run = this.ledger.db.transaction(() => {
776
741
  const existing = this.resumeTask(taskId);
777
742
  if (existing) {
778
- if (existing.contextRef !== contextRef) throw new Error('task_conflict:context_mismatch');
779
- if (existing.surface !== surface) throw new Error('task_conflict:surface_mismatch');
780
- if (existing.revision !== revision && existing.phase !== 'waiting_receipt' && !existing.replayUpgradeRequired) throw new Error('task_conflict:revision_mismatch');
781
- if (!sameActions(existing.allowedActions, allowedActions)) throw new Error('task_conflict:capability_mismatch');
782
- if (existing.phase === 'terminal' || existing.phase === 'error') throw new Error('task_terminal');
743
+ if (existing.contextRef !== contextRef)
744
+ throw new Error('task_conflict:context_mismatch');
745
+ if (existing.surface !== surface)
746
+ throw new Error('task_conflict:surface_mismatch');
747
+ if (existing.revision !== revision && existing.phase !== 'waiting_receipt' && !existing.replayUpgradeRequired)
748
+ throw new Error('task_conflict:revision_mismatch');
749
+ if (!sameActions(existing.allowedActions, allowedActions))
750
+ throw new Error('task_conflict:capability_mismatch');
751
+ if (existing.phase === 'terminal' || existing.phase === 'error')
752
+ throw new Error('task_terminal');
783
753
  if (turnId && existing.lastTurnId === turnId) {
784
754
  this.assertTurnBinding(taskId, userTurn);
785
755
  if (requestBinding) {
@@ -788,50 +758,47 @@ export class BookingCopilotTaskRuntime {
788
758
  }
789
759
  return existing;
790
760
  }
791
- if (existing.phase === 'waiting_receipt') throw new Error('receipt_required');
761
+ if (existing.phase === 'waiting_receipt')
762
+ throw new Error('receipt_required');
792
763
  const approval = userTurn && 'approval' in userTurn.request ? userTurn.request.approval : undefined;
793
764
  let approvalState;
794
765
  if (approval) {
795
766
  assertReplayUpgradeReanchored(existing);
796
767
  const awaiting = existing.awaitingApproval;
797
- if (!awaiting) throw new Error('approval_not_awaiting');
798
- if (!awaiting.optionsEmitted) throw new Error('approval_not_presented');
799
- if (Date.parse(awaiting.expiresAt) <= Date.parse(this.now())) throw new Error('approval_expired');
768
+ if (!awaiting)
769
+ throw new Error('approval_not_awaiting');
770
+ if (!awaiting.optionsEmitted)
771
+ throw new Error('approval_not_presented');
772
+ if (Date.parse(awaiting.expiresAt) <= Date.parse(this.now()))
773
+ throw new Error('approval_expired');
800
774
  const checked = validateApprovalAgainstBlocker(approval, awaiting.blocker);
801
- const option = awaiting.options.find((candidate)=>candidate.optionDigest === approval.optionDigest && bookingDigest(candidate) === bookingDigest(approval));
802
- if (!checked.ok || !option || approval.taskId !== existing.taskId || approval.contextRef !== existing.contextRef || approval.sourceTurnId !== awaiting.sourceTurnId || approval.presentationRequestKey !== awaiting.presentationRequestKey || approval.optionDigest !== approvalOptionDigest(approval) || awaiting.approval && bookingDigest(approval) !== bookingDigest(awaiting.approval)) throw new Error('approval_mismatch');
775
+ const option = awaiting.options.find((candidate) => candidate.optionDigest === approval.optionDigest && bookingDigest(candidate) === bookingDigest(approval));
776
+ if (!checked.ok || !option || approval.taskId !== existing.taskId || approval.contextRef !== existing.contextRef || approval.sourceTurnId !== awaiting.sourceTurnId || approval.presentationRequestKey !== awaiting.presentationRequestKey || approval.optionDigest !== approvalOptionDigest(approval) || (awaiting.approval && bookingDigest(approval) !== bookingDigest(awaiting.approval)))
777
+ throw new Error('approval_mismatch');
803
778
  if (awaiting.approval && bookingDigest(awaiting.approval) === bookingDigest(approval)) {
779
+ // The canonical approval may only be advanced by its original
780
+ // turn identity. A fresh key must not join the in-flight planner
781
+ // and receive the operation batch before durable consumption.
804
782
  throw new Error('approval_replayed');
805
783
  }
806
- approvalState = {
807
- ...awaiting,
808
- approval
809
- };
784
+ approvalState = { ...awaiting, approval };
810
785
  this.appendApproval(taskId, existing.contextRef, approvalState);
811
- } else if (existing.awaitingApproval) {
786
+ }
787
+ else if (existing.awaitingApproval) {
812
788
  throw new Error('approval_required');
813
789
  }
814
790
  this.appendTurn(taskId, existing.contextRef, requestDigest, workspaceDigest, workspaceSemanticDigest, existing.userTurnCount + 1, turnId, boundWorkspace);
815
- if (requestBinding) this.appendRequestBindingInTransaction(requestBinding.requestKey, userTurn, requestBinding);
791
+ if (requestBinding)
792
+ this.appendRequestBindingInTransaction(requestBinding.requestKey, userTurn, requestBinding);
816
793
  return this.requireTask(taskId);
817
794
  }
818
- if (userTurn?.request.approval) throw new Error('approval_not_awaiting');
819
- const started = {
820
- schema: LEDGER_SCHEMA,
821
- taskId,
822
- contextRef,
823
- surface,
824
- revision,
825
- allowedActions,
826
- workspaceDigest,
827
- workspaceSemanticDigest,
828
- workspace: boundWorkspace,
829
- availability,
830
- availabilityDigest: bookingDigest(availability)
831
- };
795
+ if (userTurn?.request.approval)
796
+ throw new Error('approval_not_awaiting');
797
+ const started = { schema: LEDGER_SCHEMA, taskId, contextRef, surface, revision, allowedActions, workspaceDigest, workspaceSemanticDigest, workspace: boundWorkspace, availability, availabilityDigest: bookingDigest(availability) };
832
798
  this.append(STARTED, taskId, started, `booking-copilot:task:${taskId}`);
833
799
  this.appendTurn(taskId, contextRef, requestDigest, workspaceDigest, workspaceSemanticDigest, 1, turnId, boundWorkspace);
834
- if (requestBinding) this.appendRequestBindingInTransaction(requestBinding.requestKey, userTurn, requestBinding);
800
+ if (requestBinding)
801
+ this.appendRequestBindingInTransaction(requestBinding.requestKey, userTurn, requestBinding);
835
802
  return this.requireTask(taskId);
836
803
  });
837
804
  return run.immediate();
@@ -839,108 +806,96 @@ export class BookingCopilotTaskRuntime {
839
806
  resumeTask(taskId) {
840
807
  assertTaskId(taskId);
841
808
  const rows = this.rows(taskId);
842
- if (!rows.length) return null;
809
+ if (!rows.length)
810
+ return null;
843
811
  let state = null;
844
812
  let skipLegacyUpgradeTail = false;
845
813
  const replayWorkspaceDigests = new Map();
846
- const consumed = {
847
- events: new Map(),
848
- actions: new Map(),
849
- pendingBatchKeys: new Set(),
850
- pendingBatchOpen: false
851
- };
814
+ const consumed = { events: new Map(), actions: new Map(), pendingBatchKeys: new Set(), pendingBatchOpen: false };
852
815
  const suppressedDecisionRequestKeys = new Set();
853
816
  const suppressedRequestBindingKeys = new Set();
854
817
  const suppressedReceiptActionIds = new Set();
855
818
  const suppressedApprovalTargetActionIds = new Set();
856
- for (const row of rows){
819
+ for (const row of rows) {
857
820
  const payload = JSON.parse(row.payload);
858
- if (payload.schema !== LEDGER_SCHEMA || payload.taskId !== taskId) throw new Error(`ledger_corrupt:${taskId}:seq=${row.seq}`);
821
+ if (payload.schema !== LEDGER_SCHEMA || payload.taskId !== taskId)
822
+ throw new Error(`ledger_corrupt:${taskId}:seq=${row.seq}`);
859
823
  if (row.kind === STARTED) {
860
824
  skipLegacyUpgradeTail = false;
861
- if (state) throw new Error(`ledger_corrupt:${taskId}:duplicate_start`);
825
+ if (state)
826
+ throw new Error(`ledger_corrupt:${taskId}:duplicate_start`);
862
827
  const started = payload;
863
828
  let startedWorkspace;
864
829
  try {
865
830
  startedWorkspace = normalizeWorkspaceForReplay(started.workspace);
866
- } catch {
831
+ }
832
+ catch {
867
833
  throw new Error(`ledger_corrupt:${taskId}:start_workspace`);
868
834
  }
869
835
  let initialAvailability;
870
836
  try {
871
837
  initialAvailability = createAvailabilityPolicy(startedWorkspace);
872
- } catch {
838
+ }
839
+ catch {
873
840
  throw new Error(`ledger_corrupt:${taskId}:start_availability`);
874
841
  }
875
842
  try {
876
843
  assertWorkspaceLoadedOfferRefsUnique(startedWorkspace);
877
- } catch {
844
+ }
845
+ catch {
878
846
  throw new Error(`ledger_corrupt:${taskId}:start_workspace`);
879
847
  }
880
848
  rememberReplayWorkspaceDigest(replayWorkspaceDigests, started.workspaceDigest, started.workspace, startedWorkspace);
881
849
  const startedAvailability = normalizeAvailabilityPolicyForReplay(started.availability, startedWorkspace, undefined, undefined, replayWorkspaceDigests);
882
- if (!started.workspaceDigest || !started.workspaceSemanticDigest || !started.workspace || !started.availability || !started.availabilityDigest || !validateAvailabilityPolicy(startedAvailability) || !replayDigestMatches(started.availabilityDigest, started.availability, startedAvailability) || bookingDigest(initialAvailability) !== bookingDigest(startedAvailability) || !replayWorkspaceDigestMatches(started.workspaceDigest, started.workspace, startedWorkspace) || !replayWorkspaceSemanticDigestMatches(started.workspaceSemanticDigest, started.workspace, startedWorkspace)) throw new Error(`ledger_corrupt:${taskId}:start_workspace`);
883
- state = {
884
- schemaVersion: 'booking.surface',
885
- taskId,
886
- contextRef: started.contextRef,
887
- surface: started.surface,
888
- revision: started.revision,
889
- allowedActions: [
890
- ...started.allowedActions
891
- ],
892
- userTurnCount: 0,
893
- operationCount: 0,
894
- phase: startedAvailability.terminal ? 'terminal' : 'planning',
895
- lastSequence: 0,
896
- workspaceDigest: bookingWorkspaceDigest(startedWorkspace),
897
- workspaceSemanticDigest: bookingWorkspaceSemanticDigest(startedWorkspace),
898
- workspaceSnapshot: startedWorkspace,
899
- availability: startedAvailability
900
- };
901
- } else {
902
- if (!state) throw new Error(`ledger_corrupt:${taskId}:event_before_start`);
903
- if (payload.contextRef !== state.contextRef) throw new Error(`ledger_corrupt:${taskId}:context_drift`);
850
+ if (!started.workspaceDigest || !started.workspaceSemanticDigest || !started.workspace || !started.availability || !started.availabilityDigest || !validateAvailabilityPolicy(startedAvailability) || !replayDigestMatches(started.availabilityDigest, started.availability, startedAvailability) || bookingDigest(initialAvailability) !== bookingDigest(startedAvailability) || !replayWorkspaceDigestMatches(started.workspaceDigest, started.workspace, startedWorkspace) || !replayWorkspaceSemanticDigestMatches(started.workspaceSemanticDigest, started.workspace, startedWorkspace))
851
+ throw new Error(`ledger_corrupt:${taskId}:start_workspace`);
852
+ state = { schemaVersion: 'booking.surface', taskId, contextRef: started.contextRef, surface: started.surface, revision: started.revision, allowedActions: [...started.allowedActions], userTurnCount: 0, operationCount: 0, phase: startedAvailability.terminal ? 'terminal' : 'planning', lastSequence: 0, workspaceDigest: bookingWorkspaceDigest(startedWorkspace), workspaceSemanticDigest: bookingWorkspaceSemanticDigest(startedWorkspace), workspaceSnapshot: startedWorkspace, availability: startedAvailability };
853
+ }
854
+ else {
855
+ if (!state)
856
+ throw new Error(`ledger_corrupt:${taskId}:event_before_start`);
857
+ if (payload.contextRef !== state.contextRef)
858
+ throw new Error(`ledger_corrupt:${taskId}:context_drift`);
904
859
  if (row.kind === REQUEST_BINDING) {
905
860
  if (skipLegacyUpgradeTail) {
906
861
  const binding = payload;
907
- if (typeof binding.requestKey === 'string') suppressedRequestBindingKeys.add(binding.requestKey);
862
+ if (typeof binding.requestKey === 'string')
863
+ suppressedRequestBindingKeys.add(binding.requestKey);
908
864
  continue;
909
865
  }
910
866
  const binding = payload;
911
867
  const turnRows = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ?`).all(this.ledger.tenant, taskId, TURN);
912
- const rawBoundTurn = turnRows.map(({ payload })=>JSON.parse(payload)).find((candidate)=>candidate.turnId === binding.turnId);
868
+ const rawBoundTurn = turnRows.map(({ payload }) => JSON.parse(payload)).find((candidate) => candidate.turnId === binding.turnId);
913
869
  let boundTurn;
914
870
  try {
915
- boundTurn = rawBoundTurn ? {
916
- ...rawBoundTurn,
917
- workspace: normalizeWorkspaceForReplay(rawBoundTurn.workspace)
918
- } : undefined;
919
- } catch {
871
+ boundTurn = rawBoundTurn ? { ...rawBoundTurn, workspace: normalizeWorkspaceForReplay(rawBoundTurn.workspace) } : undefined;
872
+ }
873
+ catch {
920
874
  throw new Error(`ledger_corrupt:${taskId}:request_binding`);
921
875
  }
922
- if (!isSafeBookingRequestKey(binding.requestKey) || !safeIdentity(binding.turnId) || !safeIdentity(binding.contextRef) || binding.taskHandle !== undefined && !safeIdentity(binding.taskHandle) || !/^[0-9a-f]{64}$/.test(binding.requestDigest) || !/^[0-9a-f]{64}$/.test(binding.workspaceDigest) || !/^[0-9a-f]{64}$/.test(binding.workspaceSemanticDigest) || !/^[0-9a-f]{64}$/.test(binding.capabilityDigest) || !/^[0-9a-f]{64}$/.test(binding.principalDigest) || !/^[0-9a-f]{64}$/.test(binding.scopeDigest) || ![
923
- 'tenant',
924
- 'customer_portal',
925
- 'storefront',
926
- 'payment_link'
927
- ].includes(binding.surface) || !boundTurn || boundTurn.requestDigest !== binding.requestDigest || boundTurn.workspaceDigest !== binding.workspaceDigest || boundTurn.workspaceSemanticDigest !== binding.workspaceSemanticDigest || bookingDigest(boundTurn.workspace.capabilities) !== binding.capabilityDigest || boundTurn.workspace.surface !== binding.surface) throw new Error(`ledger_corrupt:${taskId}:request_binding`);
928
- } else if (row.kind === TURN) {
876
+ if (!isSafeBookingRequestKey(binding.requestKey) || !safeIdentity(binding.turnId) || !safeIdentity(binding.contextRef) || (binding.taskHandle !== undefined && !safeIdentity(binding.taskHandle)) || !/^[0-9a-f]{64}$/.test(binding.requestDigest) || !/^[0-9a-f]{64}$/.test(binding.workspaceDigest) || !/^[0-9a-f]{64}$/.test(binding.workspaceSemanticDigest) || !/^[0-9a-f]{64}$/.test(binding.capabilityDigest) || !/^[0-9a-f]{64}$/.test(binding.principalDigest) || !/^[0-9a-f]{64}$/.test(binding.scopeDigest) || !['tenant', 'customer_portal', 'storefront', 'payment_link'].includes(binding.surface) || !boundTurn || boundTurn.requestDigest !== binding.requestDigest || boundTurn.workspaceDigest !== binding.workspaceDigest || boundTurn.workspaceSemanticDigest !== binding.workspaceSemanticDigest || bookingDigest(boundTurn.workspace.capabilities) !== binding.capabilityDigest || boundTurn.workspace.surface !== binding.surface)
877
+ throw new Error(`ledger_corrupt:${taskId}:request_binding`);
878
+ }
879
+ else if (row.kind === TURN) {
929
880
  skipLegacyUpgradeTail = false;
930
881
  const t = payload;
931
- if (Object.prototype.hasOwnProperty.call(payload, 'approval')) throw new Error(`ledger_corrupt:${taskId}:turn_approval`);
882
+ if (Object.prototype.hasOwnProperty.call(payload, 'approval'))
883
+ throw new Error(`ledger_corrupt:${taskId}:turn_approval`);
932
884
  let turnWorkspace;
933
885
  try {
934
886
  turnWorkspace = normalizeWorkspaceForReplay(t.workspace);
935
- } catch {
887
+ }
888
+ catch {
936
889
  throw new Error(`ledger_corrupt:${taskId}:turn_workspace`);
937
890
  }
938
891
  try {
939
892
  assertWorkspaceLoadedOfferRefsUnique(turnWorkspace);
940
- } catch {
893
+ }
894
+ catch {
941
895
  throw new Error(`ledger_corrupt:${taskId}:turn_workspace`);
942
896
  }
943
- if (!t.workspaceDigest || !t.workspaceSemanticDigest || !t.workspace || !replayWorkspaceDigestMatches(t.workspaceDigest, t.workspace, turnWorkspace) || !replayWorkspaceSemanticDigestMatches(t.workspaceSemanticDigest, t.workspace, turnWorkspace)) throw new Error(`ledger_corrupt:${taskId}:turn_workspace`);
897
+ if (!t.workspaceDigest || !t.workspaceSemanticDigest || !t.workspace || !replayWorkspaceDigestMatches(t.workspaceDigest, t.workspace, turnWorkspace) || !replayWorkspaceSemanticDigestMatches(t.workspaceSemanticDigest, t.workspace, turnWorkspace))
898
+ throw new Error(`ledger_corrupt:${taskId}:turn_workspace`);
944
899
  rememberReplayWorkspaceDigest(replayWorkspaceDigests, t.workspaceDigest, t.workspace, turnWorkspace);
945
900
  state.userTurnCount++;
946
901
  state.lastTurnId = t.turnId;
@@ -949,23 +904,37 @@ export class BookingCopilotTaskRuntime {
949
904
  state.workspaceSnapshot = turnWorkspace;
950
905
  state.revision = turnWorkspace.revision;
951
906
  delete state.replayUpgradeRequired;
952
- } else if (row.kind === APPROVAL_GRANTED) {
953
- if (skipLegacyUpgradeTail) continue;
907
+ // Approval authority is established only by APPROVAL_GRANTED; a turn
908
+ // is merely an observation and cannot restore or mutate approval state.
909
+ }
910
+ else if (row.kind === APPROVAL_GRANTED) {
911
+ if (skipLegacyUpgradeTail)
912
+ continue;
954
913
  const granted = payload;
955
- if (!granted.approval || granted.approvalDigest !== bookingDigest(granted.approval) || !granted.approval.sourceTurnId || !granted.approval.presentationRequestKey || !validateCriterionBlocker(granted.approval.blocker).ok || !granted.approval.options?.length || granted.approval.options.some((option)=>!validateApprovalAgainstBlocker(option, granted.approval.blocker).ok || option.taskId !== taskId || option.contextRef !== state.contextRef || option.sourceTurnId !== granted.approval.sourceTurnId || option.presentationRequestKey !== granted.approval.presentationRequestKey || option.optionDigest !== approvalOptionDigest(option)) || granted.approval.approval && !granted.approval.options.some((option)=>bookingDigest(option) === bookingDigest(granted.approval.approval))) throw new Error(`ledger_corrupt:${taskId}:approval_granted`);
914
+ if (!granted.approval || granted.approvalDigest !== bookingDigest(granted.approval) || !granted.approval.sourceTurnId || !granted.approval.presentationRequestKey || !validateCriterionBlocker(granted.approval.blocker).ok || !granted.approval.options?.length || granted.approval.options.some((option) => !validateApprovalAgainstBlocker(option, granted.approval.blocker).ok || option.taskId !== taskId || option.contextRef !== state.contextRef || option.sourceTurnId !== granted.approval.sourceTurnId || option.presentationRequestKey !== granted.approval.presentationRequestKey || option.optionDigest !== approvalOptionDigest(option)) || (granted.approval.approval && !granted.approval.options.some((option) => bookingDigest(option) === bookingDigest(granted.approval.approval))))
915
+ throw new Error(`ledger_corrupt:${taskId}:approval_granted`);
956
916
  state.awaitingApproval = granted.approval;
957
917
  state.phase = 'input_required';
958
- } else if (row.kind === APPROVAL_OFFERED) {
959
- if (skipLegacyUpgradeTail) continue;
918
+ }
919
+ else if (row.kind === APPROVAL_OFFERED) {
920
+ if (skipLegacyUpgradeTail)
921
+ continue;
960
922
  const offered = payload;
961
- if (!offered.approval || offered.approvalDigest !== bookingDigest(offered.approval) || !state.awaitingApproval || bookingDigest(offered.approval) !== bookingDigest(state.awaitingApproval)) throw new Error(`ledger_corrupt:${taskId}:approval_offered`);
923
+ if (!offered.approval || offered.approvalDigest !== bookingDigest(offered.approval) || !state.awaitingApproval || bookingDigest(offered.approval) !== bookingDigest(state.awaitingApproval))
924
+ throw new Error(`ledger_corrupt:${taskId}:approval_offered`);
925
+ // This is a durable outbox intent only. It is deliberately not
926
+ // evidence that the question entered a replayable output batch:
927
+ // a crash between this row and DECISION_BATCH must still reject an
928
+ // approval until the exact question can be replayed.
962
929
  state.phase = 'input_required';
963
- } else if (row.kind === ACTION) {
930
+ }
931
+ else if (row.kind === ACTION) {
964
932
  const actionPayload = payload;
965
933
  if (skipLegacyUpgradeTail) {
966
934
  try {
967
935
  consumeSkippedActionOrdinal(state, actionPayload, consumed);
968
- } catch {
936
+ }
937
+ catch {
969
938
  throw new Error(`ledger_corrupt:${taskId}:action`);
970
939
  }
971
940
  continue;
@@ -973,16 +942,19 @@ export class BookingCopilotTaskRuntime {
973
942
  const rawAction = actionPayload.action;
974
943
  const operationCount = actionPayload.operationCount ?? state.operationCount + 1;
975
944
  const { actionDigest: _rawActionDigest, ...rawActionBase } = rawAction;
976
- if (rawAction.actionDigest !== checkpointDigest(rawActionBase) || rawAction.contextRef !== state.contextRef || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount + 1 || operationCount > BOOKING_COPILOT_MAX_OPERATIONS || !Number.isSafeInteger(rawAction.sequence) || rawAction.sequence <= state.lastSequence) throw new Error(`ledger_corrupt:${taskId}:action`);
945
+ if (rawAction.actionDigest !== checkpointDigest(rawActionBase) || rawAction.contextRef !== state.contextRef || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount + 1 || operationCount > BOOKING_COPILOT_MAX_OPERATIONS || !Number.isSafeInteger(rawAction.sequence) || rawAction.sequence <= state.lastSequence)
946
+ throw new Error(`ledger_corrupt:${taskId}:action`);
977
947
  let a;
978
948
  try {
979
949
  a = normalizeActionCheckpointForReplay(rawAction, state.workspaceSnapshot);
980
- } catch (error) {
950
+ }
951
+ catch (error) {
981
952
  if (error instanceof Error && error.message.startsWith('booking_replan_required:')) {
982
953
  state = replanAfterReplayUpgradeGap(state);
983
954
  try {
984
955
  consumeSkippedActionOrdinal(state, actionPayload, consumed);
985
- } catch {
956
+ }
957
+ catch {
986
958
  throw new Error(`ledger_corrupt:${taskId}:action`);
987
959
  }
988
960
  skipLegacyUpgradeTail = true;
@@ -991,16 +963,19 @@ export class BookingCopilotTaskRuntime {
991
963
  throw error;
992
964
  }
993
965
  const { actionDigest: _actionDigest, ...actionBase } = a;
994
- if (!actionPayload.availability || actionPayload.availabilityDigest !== bookingDigest(actionPayload.availability)) throw new Error(`ledger_corrupt:${taskId}:action`);
966
+ if (!actionPayload.availability || actionPayload.availabilityDigest !== bookingDigest(actionPayload.availability))
967
+ throw new Error(`ledger_corrupt:${taskId}:action`);
995
968
  let actionAvailability;
996
969
  try {
997
970
  actionAvailability = normalizeAvailabilityPolicyForReplay(actionPayload.availability, state.workspaceSnapshot, a, state.availability, replayWorkspaceDigests);
998
- } catch (error) {
971
+ }
972
+ catch (error) {
999
973
  if (error instanceof Error && error.message.startsWith('booking_replan_required:')) {
1000
974
  state = replanAfterReplayUpgradeGap(state);
1001
975
  try {
1002
976
  consumeSkippedActionOrdinal(state, actionPayload, consumed);
1003
- } catch {
977
+ }
978
+ catch {
1004
979
  throw new Error(`ledger_corrupt:${taskId}:action`);
1005
980
  }
1006
981
  skipLegacyUpgradeTail = true;
@@ -1008,16 +983,21 @@ export class BookingCopilotTaskRuntime {
1008
983
  }
1009
984
  throw error;
1010
985
  }
1011
- if (!checkpointDigestMatchesForReplay(rawAction, a) || a.actionDigest !== checkpointDigest(actionBase) || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount + 1 || operationCount > BOOKING_COPILOT_MAX_OPERATIONS || !validateAvailabilityPolicy(actionAvailability) || !replayDigestMatches(actionPayload.availabilityDigest, actionPayload.availability, actionAvailability) || !state.workspaceSnapshot) throw new Error(`ledger_corrupt:${taskId}:action`);
1012
- if (a.relaxationApprovalRef) assertApprovalRef(a.relaxationApprovalRef, state, a);
1013
- if (state.pendingAction) throw new Error(`ledger_corrupt:${taskId}:parallel_actions`);
986
+ if (!checkpointDigestMatchesForReplay(rawAction, a) || a.actionDigest !== checkpointDigest(actionBase) || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount + 1 || operationCount > BOOKING_COPILOT_MAX_OPERATIONS || !validateAvailabilityPolicy(actionAvailability) || !replayDigestMatches(actionPayload.availabilityDigest, actionPayload.availability, actionAvailability) || !state.workspaceSnapshot)
987
+ throw new Error(`ledger_corrupt:${taskId}:action`);
988
+ if (a.relaxationApprovalRef)
989
+ assertApprovalRef(a.relaxationApprovalRef, state, a);
990
+ if (state.pendingAction)
991
+ throw new Error(`ledger_corrupt:${taskId}:parallel_actions`);
1014
992
  let expectedAvailability;
1015
993
  try {
1016
994
  expectedAvailability = reduceAvailabilityAction(state.availability, state.workspaceSnapshot, a);
1017
- } catch {
995
+ }
996
+ catch {
1018
997
  throw new Error(`ledger_corrupt:${taskId}:action_transition`);
1019
998
  }
1020
- if (bookingDigest(expectedAvailability) !== bookingDigest(actionAvailability)) throw new Error(`ledger_corrupt:${taskId}:action_transition`);
999
+ if (bookingDigest(expectedAvailability) !== bookingDigest(actionAvailability))
1000
+ throw new Error(`ledger_corrupt:${taskId}:action_transition`);
1021
1001
  state.operationCount = operationCount;
1022
1002
  state.availability = actionAvailability;
1023
1003
  state.pendingAction = a;
@@ -1027,34 +1007,43 @@ export class BookingCopilotTaskRuntime {
1027
1007
  rememberConsumedActionIdentity(consumed, a);
1028
1008
  }
1029
1009
  delete state.awaitingApproval;
1030
- } else if (row.kind === RECEIPT) {
1010
+ }
1011
+ else if (row.kind === RECEIPT) {
1031
1012
  if (skipLegacyUpgradeTail) {
1032
1013
  const r = payload;
1033
- if (typeof r.receipt?.actionId === 'string') suppressedReceiptActionIds.add(r.receipt.actionId);
1014
+ if (typeof r.receipt?.actionId === 'string')
1015
+ suppressedReceiptActionIds.add(r.receipt.actionId);
1034
1016
  continue;
1035
1017
  }
1036
1018
  const r = payload;
1037
1019
  const operationCount = r.operationCount ?? state.operationCount;
1038
- if (!state.pendingAction || state.pendingAction.actionId !== r.receipt.actionId) throw new Error(`ledger_corrupt:${taskId}:orphan_receipt`);
1020
+ if (!state.pendingAction || state.pendingAction.actionId !== r.receipt.actionId)
1021
+ throw new Error(`ledger_corrupt:${taskId}:orphan_receipt`);
1039
1022
  let receiptWorkspace;
1040
1023
  try {
1041
1024
  receiptWorkspace = normalizeWorkspaceForReplay(r.workspace);
1042
- } catch {
1025
+ }
1026
+ catch {
1043
1027
  throw new Error(`ledger_corrupt:${taskId}:receipt_workspace`);
1044
1028
  }
1045
1029
  try {
1046
1030
  assertWorkspaceLoadedOfferRefsUnique(receiptWorkspace);
1047
- } catch {
1031
+ }
1032
+ catch {
1048
1033
  throw new Error(`ledger_corrupt:${taskId}:receipt_workspace`);
1049
1034
  }
1050
- if (!r.workspaceDigest || !r.workspaceSemanticDigest || !r.workspace || !replayWorkspaceDigestMatches(r.workspaceDigest, r.workspace, receiptWorkspace) || !replayWorkspaceSemanticDigestMatches(r.workspaceSemanticDigest, r.workspace, receiptWorkspace)) throw new Error(`ledger_corrupt:${taskId}:receipt_workspace`);
1035
+ if (!r.workspaceDigest || !r.workspaceSemanticDigest || !r.workspace || !replayWorkspaceDigestMatches(r.workspaceDigest, r.workspace, receiptWorkspace) || !replayWorkspaceSemanticDigestMatches(r.workspaceSemanticDigest, r.workspace, receiptWorkspace))
1036
+ throw new Error(`ledger_corrupt:${taskId}:receipt_workspace`);
1051
1037
  rememberReplayWorkspaceDigest(replayWorkspaceDigests, r.workspaceDigest, r.workspace, receiptWorkspace);
1052
- if (!replayDigestMatches(r.receiptDigest, r.receipt, r.receipt) || r.receipt.contextRef !== state.contextRef || r.receipt.actionId !== state.pendingAction.actionId || !Number.isSafeInteger(r.receipt.revision) || r.receipt.revision < state.pendingAction.expectedRevision || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount) throw new Error(`ledger_corrupt:${taskId}:receipt`);
1053
- if (!r.availability || r.availabilityDigest !== bookingDigest(r.availability)) throw new Error(`ledger_corrupt:${taskId}:receipt`);
1038
+ if (!replayDigestMatches(r.receiptDigest, r.receipt, r.receipt) || r.receipt.contextRef !== state.contextRef || r.receipt.actionId !== state.pendingAction.actionId || !Number.isSafeInteger(r.receipt.revision) || r.receipt.revision < state.pendingAction.expectedRevision || !Number.isSafeInteger(operationCount) || operationCount !== state.operationCount)
1039
+ throw new Error(`ledger_corrupt:${taskId}:receipt`);
1040
+ if (!r.availability || r.availabilityDigest !== bookingDigest(r.availability))
1041
+ throw new Error(`ledger_corrupt:${taskId}:receipt`);
1054
1042
  let receipt;
1055
1043
  try {
1056
1044
  receipt = normalizeReceiptForReplay(r.receipt, state.pendingAction, receiptWorkspace);
1057
- } catch (error) {
1045
+ }
1046
+ catch (error) {
1058
1047
  if (error instanceof Error && error.message.startsWith('booking_replan_required:')) {
1059
1048
  clearPendingBatch(consumed);
1060
1049
  state = replanAfterReplayUpgradeGap(state, receiptWorkspace, r.receipt.revision);
@@ -1066,7 +1055,8 @@ export class BookingCopilotTaskRuntime {
1066
1055
  let receiptAvailability;
1067
1056
  try {
1068
1057
  receiptAvailability = normalizeAvailabilityPolicyForReplay(r.availability, receiptWorkspace, state.pendingAction, state.availability, replayWorkspaceDigests, receipt);
1069
- } catch (error) {
1058
+ }
1059
+ catch (error) {
1070
1060
  if (error instanceof Error && error.message.startsWith('booking_replan_required:')) {
1071
1061
  clearPendingBatch(consumed);
1072
1062
  state = replanAfterReplayUpgradeGap(state, receiptWorkspace, r.receipt.revision);
@@ -1075,17 +1065,22 @@ export class BookingCopilotTaskRuntime {
1075
1065
  }
1076
1066
  throw error;
1077
1067
  }
1078
- if (!replayDigestMatches(r.receiptDigest, r.receipt, receipt) || !validateBookingSurface(receipt).ok || !r.availability || !r.availabilityDigest || !validateAvailabilityPolicy(receiptAvailability) || !replayDigestMatches(r.availabilityDigest, r.availability, receiptAvailability)) throw new Error(`ledger_corrupt:${taskId}:receipt`);
1079
- if (Boolean(r.availabilityTerminal) !== Boolean(receiptAvailability.terminal) || r.availabilityTerminal && receiptAvailability.terminal && bookingDigest(r.availabilityTerminal) !== bookingDigest(receiptAvailability.terminal)) throw new Error(`ledger_corrupt:${taskId}:availability_terminal`);
1068
+ if (!replayDigestMatches(r.receiptDigest, r.receipt, receipt) || !validateBookingSurface(receipt).ok || !r.availability || !r.availabilityDigest || !validateAvailabilityPolicy(receiptAvailability) || !replayDigestMatches(r.availabilityDigest, r.availability, receiptAvailability))
1069
+ throw new Error(`ledger_corrupt:${taskId}:receipt`);
1070
+ if (Boolean(r.availabilityTerminal) !== Boolean(receiptAvailability.terminal) || (r.availabilityTerminal && receiptAvailability.terminal && bookingDigest(r.availabilityTerminal) !== bookingDigest(receiptAvailability.terminal)))
1071
+ throw new Error(`ledger_corrupt:${taskId}:availability_terminal`);
1080
1072
  assertCanonicalReceipt(receipt);
1081
- if (!state.workspaceSnapshot || !workspacePostActionMatches(state.workspaceSnapshot, receiptWorkspace, state.pendingAction, receipt) || !receiptObservationMatchesAction(receipt.observation.kind, state.pendingAction.kind) || !receiptTargetMatchesAction(receipt, state.pendingAction)) throw new Error(`ledger_corrupt:${taskId}:receipt_transition`);
1073
+ if (!state.workspaceSnapshot || !workspacePostActionMatches(state.workspaceSnapshot, receiptWorkspace, state.pendingAction, receipt) || !receiptObservationMatchesAction(receipt.observation.kind, state.pendingAction.kind) || !receiptTargetMatchesAction(receipt, state.pendingAction))
1074
+ throw new Error(`ledger_corrupt:${taskId}:receipt_transition`);
1082
1075
  let expectedAvailability;
1083
1076
  try {
1084
1077
  expectedAvailability = reduceAvailabilityReceipt(state.availability, receiptWorkspace, receipt, state.pendingAction);
1085
- } catch {
1078
+ }
1079
+ catch {
1086
1080
  throw new Error(`ledger_corrupt:${taskId}:receipt_transition`);
1087
1081
  }
1088
- if (bookingDigest(expectedAvailability) !== bookingDigest(receiptAvailability)) throw new Error(`ledger_corrupt:${taskId}:receipt_transition`);
1082
+ if (bookingDigest(expectedAvailability) !== bookingDigest(receiptAvailability))
1083
+ throw new Error(`ledger_corrupt:${taskId}:receipt_transition`);
1089
1084
  state.lastReceipt = receipt;
1090
1085
  state.revision = receipt.revision;
1091
1086
  state.workspaceDigest = bookingWorkspaceDigest(receiptWorkspace);
@@ -1096,45 +1091,59 @@ export class BookingCopilotTaskRuntime {
1096
1091
  state.phase = receiptAvailability.terminal || state.operationCount >= BOOKING_COPILOT_MAX_OPERATIONS ? 'terminal' : 'planning';
1097
1092
  clearPendingBatch(consumed);
1098
1093
  delete state.awaitingApproval;
1099
- } else if (row.kind === APPROVAL_CONSUMED) {
1094
+ }
1095
+ else if (row.kind === APPROVAL_CONSUMED) {
1100
1096
  if (skipLegacyUpgradeTail) {
1101
1097
  const consumed = payload;
1102
- if (typeof consumed.ref?.targetActionId === 'string') suppressedApprovalTargetActionIds.add(consumed.ref.targetActionId);
1098
+ if (typeof consumed.ref?.targetActionId === 'string')
1099
+ suppressedApprovalTargetActionIds.add(consumed.ref.targetActionId);
1103
1100
  continue;
1104
1101
  }
1105
1102
  const consumed = payload;
1106
- if (!consumed.ref || consumed.approvalDigest !== bookingDigest(consumed.ref) || !consumed.approval?.approval || !consumed.approval.options?.some((option)=>bookingDigest(option) === bookingDigest(consumed.approval.approval)) || !state.awaitingApproval || bookingDigest(consumed.approval) !== bookingDigest(state.awaitingApproval) || consumed.ref.approvalId !== consumed.approval.approval.approvalId || consumed.ref.blockerId !== consumed.approval.blocker.blockerId || consumed.ref.contextRef !== state.contextRef || consumed.ref.sourceTurnId !== consumed.approval.sourceTurnId || consumed.ref.presentationRequestKey !== consumed.approval.presentationRequestKey || consumed.ref.sourceActionId !== consumed.approval.blocker.sourceActionId || consumed.ref.targetActionId === '' || consumed.ref.sourceReceiptDigest !== consumed.approval.blocker.sourceReceiptDigest || consumed.ref.nonce !== consumed.approval.nonce || consumed.ref.nonce !== consumed.approval.approval.deliveryNonce || consumed.ref.to !== consumed.approval.approval.to || consumed.ref.scope !== consumed.approval.blocker.scope || consumed.ref.code !== consumed.approval.blocker.code || consumed.ref.criterionPath !== consumed.approval.blocker.criterionPath || consumed.ref.valueDigest !== consumed.approval.blocker.valueDigest) throw new Error(`ledger_corrupt:${taskId}:approval`);
1107
- } else if (row.kind === DECISION_BATCH) {
1103
+ if (!consumed.ref || consumed.approvalDigest !== bookingDigest(consumed.ref) || !consumed.approval?.approval || !consumed.approval.options?.some((option) => bookingDigest(option) === bookingDigest(consumed.approval.approval)) || !state.awaitingApproval || bookingDigest(consumed.approval) !== bookingDigest(state.awaitingApproval) || consumed.ref.approvalId !== consumed.approval.approval.approvalId || consumed.ref.blockerId !== consumed.approval.blocker.blockerId || consumed.ref.contextRef !== state.contextRef || consumed.ref.sourceTurnId !== consumed.approval.sourceTurnId || consumed.ref.presentationRequestKey !== consumed.approval.presentationRequestKey || consumed.ref.sourceActionId !== consumed.approval.blocker.sourceActionId || consumed.ref.targetActionId === '' || consumed.ref.sourceReceiptDigest !== consumed.approval.blocker.sourceReceiptDigest || consumed.ref.nonce !== consumed.approval.nonce || consumed.ref.nonce !== consumed.approval.approval.deliveryNonce || consumed.ref.to !== consumed.approval.approval.to || consumed.ref.scope !== consumed.approval.blocker.scope || consumed.ref.code !== consumed.approval.blocker.code || consumed.ref.criterionPath !== consumed.approval.blocker.criterionPath || consumed.ref.valueDigest !== consumed.approval.blocker.valueDigest)
1104
+ throw new Error(`ledger_corrupt:${taskId}:approval`);
1105
+ }
1106
+ else if (row.kind === DECISION_BATCH) {
1108
1107
  const batch = payload;
1109
1108
  if (skipLegacyUpgradeTail) {
1110
1109
  try {
1111
1110
  consumeSkippedEventSequence(state, batch, consumed);
1112
- } catch {
1111
+ }
1112
+ catch {
1113
1113
  throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1114
1114
  }
1115
- if (typeof batch.requestKey === 'string') suppressedDecisionRequestKeys.add(batch.requestKey);
1115
+ if (typeof batch.requestKey === 'string')
1116
+ suppressedDecisionRequestKeys.add(batch.requestKey);
1116
1117
  continue;
1117
1118
  }
1118
- if (!batch.requestKey || !Array.isArray(batch.events) || batch.events.some((event)=>event.taskId !== taskId)) throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1119
+ if (!batch.requestKey || !Array.isArray(batch.events) || batch.events.some((event) => event.taskId !== taskId))
1120
+ throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1119
1121
  let batchEvents;
1120
1122
  try {
1121
1123
  batchEvents = normalizeDecisionBatchEnvelopeForReplay(batch, consumed, true);
1122
- } catch {
1124
+ }
1125
+ catch {
1123
1126
  throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1124
1127
  }
1125
- for (const event of batchEvents){
1126
- if (event.kind !== 'question') continue;
1128
+ for (const event of batchEvents) {
1129
+ if (event.kind !== 'question')
1130
+ continue;
1127
1131
  const awaiting = state.awaitingApproval;
1128
- if (!awaiting || bookingDigest(event.question.blocker) !== bookingDigest(awaiting.blocker) || event.question.approvalOptions.length !== awaiting.options.length || event.question.approvalOptions.some(({ approval })=>!awaiting.options.some((option)=>bookingDigest(option) === bookingDigest(approval)))) throw new Error(`ledger_corrupt:${taskId}:decision_batch_question`);
1132
+ if (!awaiting || bookingDigest(event.question.blocker) !== bookingDigest(awaiting.blocker) || event.question.approvalOptions.length !== awaiting.options.length || event.question.approvalOptions.some(({ approval }) => !awaiting.options.some((option) => bookingDigest(option) === bookingDigest(approval))))
1133
+ throw new Error(`ledger_corrupt:${taskId}:decision_batch_question`);
1134
+ // Only the atomic, durable batch makes the presentation nonce
1135
+ // usable. APPROVAL_OFFERED alone is intentionally insufficient.
1129
1136
  awaiting.optionsEmitted = true;
1130
1137
  state.phase = 'input_required';
1131
1138
  }
1132
- } else if (row.kind === EVENT) {
1139
+ }
1140
+ else if (row.kind === EVENT) {
1133
1141
  const event = payload;
1134
1142
  if (skipLegacyUpgradeTail) {
1135
1143
  try {
1136
1144
  consumeSkippedEventSequence(state, event, consumed);
1137
- } catch {
1145
+ }
1146
+ catch {
1138
1147
  throw new Error(`ledger_corrupt:${taskId}:event`);
1139
1148
  }
1140
1149
  continue;
@@ -1143,69 +1152,59 @@ export class BookingCopilotTaskRuntime {
1143
1152
  {
1144
1153
  rememberConsumedEventIdentity(consumed, event);
1145
1154
  }
1146
- if (event.eventKind === 'status') state.phase = event.status === 'submitted' ? 'submitted' : event.status === 'working' ? 'working' : event.status === 'waiting_receipt' ? 'waiting_receipt' : 'input_required';
1147
- else if (event.eventKind === 'question') state.phase = 'input_required';
1148
- else if (event.eventKind === 'terminal') state.phase = 'terminal';
1149
- else if (event.eventKind === 'error') state.phase = 'error';
1155
+ if (event.eventKind === 'status')
1156
+ state.phase = event.status === 'submitted' ? 'submitted' : event.status === 'working' ? 'working' : event.status === 'waiting_receipt' ? 'waiting_receipt' : 'input_required';
1157
+ else if (event.eventKind === 'question')
1158
+ state.phase = 'input_required';
1159
+ else if (event.eventKind === 'terminal')
1160
+ state.phase = 'terminal';
1161
+ else if (event.eventKind === 'error')
1162
+ state.phase = 'error';
1150
1163
  }
1151
1164
  }
1152
1165
  }
1153
1166
  if (state) {
1154
- if (suppressedDecisionRequestKeys.size) state.legacySuppressedDecisionRequestKeys = [
1155
- ...suppressedDecisionRequestKeys
1156
- ];
1157
- if (suppressedRequestBindingKeys.size) state.legacySuppressedRequestBindingKeys = [
1158
- ...suppressedRequestBindingKeys
1159
- ];
1160
- if (suppressedReceiptActionIds.size) state.legacySuppressedReceiptActionIds = [
1161
- ...suppressedReceiptActionIds
1162
- ];
1163
- if (suppressedApprovalTargetActionIds.size) state.legacySuppressedApprovalTargetActionIds = [
1164
- ...suppressedApprovalTargetActionIds
1165
- ];
1167
+ if (suppressedDecisionRequestKeys.size)
1168
+ state.legacySuppressedDecisionRequestKeys = [...suppressedDecisionRequestKeys];
1169
+ if (suppressedRequestBindingKeys.size)
1170
+ state.legacySuppressedRequestBindingKeys = [...suppressedRequestBindingKeys];
1171
+ if (suppressedReceiptActionIds.size)
1172
+ state.legacySuppressedReceiptActionIds = [...suppressedReceiptActionIds];
1173
+ if (suppressedApprovalTargetActionIds.size)
1174
+ state.legacySuppressedApprovalTargetActionIds = [...suppressedApprovalTargetActionIds];
1166
1175
  }
1167
1176
  return state;
1168
1177
  }
1169
1178
  withReceiptDigest(receipt) {
1179
+ // The source receipt digest is the authority used by the approval binding.
1170
1180
  const sourceDigest = receiptSourceDigest(receipt);
1171
- return {
1172
- ...receipt,
1173
- resultContract: {
1174
- ...receipt.resultContract,
1175
- blockers: receipt.resultContract.blockers.map((b)=>({
1176
- ...b,
1177
- sourceReceiptDigest: sourceDigest
1178
- }))
1179
- }
1180
- };
1181
- }
1182
- receiptDigest(receipt) {
1183
- return canonicalReceiptDigest(receipt);
1181
+ return { ...receipt, resultContract: { ...receipt.resultContract, blockers: receipt.resultContract.blockers.map((b) => ({ ...b, sourceReceiptDigest: sourceDigest })) } };
1184
1182
  }
1183
+ receiptDigest(receipt) { return canonicalReceiptDigest(receipt); }
1184
+ /** Builds the only relaxation question exposed to the planner stream. The
1185
+ * blocker is ledger-derived; a planner cannot invent its tuple or ref. */
1185
1186
  approvalQuestion(taskId) {
1186
1187
  const task = this.requireTask(taskId);
1187
1188
  const awaiting = task.awaitingApproval;
1188
- if (!awaiting) throw new Error('approval_not_awaiting');
1189
+ if (!awaiting)
1190
+ throw new Error('approval_not_awaiting');
1189
1191
  return {
1190
1192
  kind: 'question',
1191
1193
  question: {
1192
1194
  questionId: `question-${awaiting.blocker.blockerId}`,
1193
1195
  prompt: `Relax required criterion ${awaiting.blocker.criterionPath}?`,
1194
- missingFields: [
1195
- awaiting.blocker.criterionPath
1196
- ],
1196
+ missingFields: [awaiting.blocker.criterionPath],
1197
1197
  type: 'relaxation_approval_required',
1198
1198
  blocker: awaiting.blocker,
1199
- approvalOptions: awaiting.options.map((approval)=>({
1200
- approval
1201
- }))
1202
- }
1199
+ approvalOptions: awaiting.options.map((approval) => ({ approval })),
1200
+ },
1203
1201
  };
1204
1202
  }
1205
1203
  approvalPresentationRequestKey(taskId) {
1206
1204
  const task = this.requireTask(taskId);
1207
1205
  const awaiting = task.awaitingApproval;
1208
- if (!awaiting) throw new Error('approval_not_awaiting');
1206
+ if (!awaiting)
1207
+ throw new Error('approval_not_awaiting');
1209
1208
  return awaiting.presentationRequestKey;
1210
1209
  }
1211
1210
  commitDecisionBatch(taskId, requestKey, decisions) {
@@ -1213,292 +1212,258 @@ export class BookingCopilotTaskRuntime {
1213
1212
  }
1214
1213
  readDecisionBatch(taskId, requestKey) {
1215
1214
  const state = this.requireTask(taskId);
1216
- if (state.legacySuppressedDecisionRequestKeys?.includes(requestKey)) throw new Error('request_key_invalidated');
1215
+ if (state.legacySuppressedDecisionRequestKeys?.includes(requestKey))
1216
+ throw new Error('request_key_invalidated');
1217
1217
  const rows = this.ledger.db.prepare('SELECT seq, payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ? ORDER BY seq DESC').all(this.ledger.tenant, taskId, DECISION_BATCH);
1218
- for (const row of rows){
1218
+ for (const row of rows) {
1219
1219
  const payload = JSON.parse(row.payload);
1220
- if (payload.requestKey !== requestKey) continue;
1221
- if (payload.schema !== LEDGER_SCHEMA || payload.taskId !== taskId || payload.contextRef !== state.contextRef || !Array.isArray(payload.events) || payload.events.some((event)=>event.taskId !== taskId || event.contextRef !== state.contextRef)) throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1220
+ if (payload.requestKey !== requestKey)
1221
+ continue;
1222
+ if (payload.schema !== LEDGER_SCHEMA || payload.taskId !== taskId || payload.contextRef !== state.contextRef || !Array.isArray(payload.events) || payload.events.some((event) => event.taskId !== taskId || event.contextRef !== state.contextRef))
1223
+ throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1222
1224
  const events = this.normalizeDecisionBatchFromDurableRows(taskId, payload.events, row.seq);
1223
- if (!events) throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1224
- for (const event of events){
1225
- if (event.kind !== 'question') continue;
1225
+ if (!events)
1226
+ throw new Error(`ledger_corrupt:${taskId}:decision_batch`);
1227
+ for (const event of events) {
1228
+ if (event.kind !== 'question')
1229
+ continue;
1226
1230
  const awaiting = state.awaitingApproval;
1227
- if (!awaiting || requestKey !== awaiting.presentationRequestKey || bookingDigest(event.question.blocker) !== bookingDigest(awaiting.blocker) || event.question.approvalOptions.length !== awaiting.options.length || event.question.approvalOptions.some(({ approval })=>!awaiting.options.some((option)=>option.optionDigest === approval.optionDigest && bookingDigest(option) === bookingDigest(approval)))) throw new Error(`ledger_corrupt:${taskId}:decision_batch_question`);
1231
+ if (!awaiting || requestKey !== awaiting.presentationRequestKey || bookingDigest(event.question.blocker) !== bookingDigest(awaiting.blocker) || event.question.approvalOptions.length !== awaiting.options.length || event.question.approvalOptions.some(({ approval }) => !awaiting.options.some((option) => option.optionDigest === approval.optionDigest && bookingDigest(option) === bookingDigest(approval))))
1232
+ throw new Error(`ledger_corrupt:${taskId}:decision_batch_question`);
1228
1233
  }
1229
1234
  return events;
1230
1235
  }
1231
1236
  return null;
1232
1237
  }
1238
+ /** Materializes the durable policy/budget terminal response once, before SSE. */
1233
1239
  terminalDecisionBatch(taskId, requestKey) {
1234
1240
  assertInternalDecisionKey(requestKey);
1235
1241
  const prior = this.readDecisionBatch(taskId, requestKey);
1236
- if (prior) return prior;
1242
+ if (prior)
1243
+ return prior;
1237
1244
  const state = this.requireTask(taskId);
1238
- if (state.phase === 'terminal' || state.phase === 'error') throw new Error('task_terminal');
1245
+ if (state.phase === 'terminal' || state.phase === 'error')
1246
+ throw new Error('task_terminal');
1239
1247
  assertReplayUpgradeReanchored(state);
1240
- const run = this.ledger.db.transaction(()=>{
1248
+ const run = this.ledger.db.transaction(() => {
1241
1249
  const state = this.requireTask(taskId);
1242
- if (!taskHasTerminalBudget(state) || state.phase !== 'terminal') throw new Error('task_not_terminal');
1250
+ if (!taskHasTerminalBudget(state) || state.phase !== 'terminal')
1251
+ throw new Error('task_not_terminal');
1243
1252
  assertReplayUpgradeReanchored(state);
1244
1253
  const event = this.terminalEvent(state);
1245
1254
  this.appendTerminalEvent(taskId, event);
1246
- this.append(DECISION_BATCH, taskId, {
1247
- schema: LEDGER_SCHEMA,
1248
- taskId,
1249
- contextRef: state.contextRef,
1250
- requestKey,
1251
- events: [
1252
- event
1253
- ]
1254
- }, `booking-copilot:decision-batch:${taskId}:${requestKey}`);
1255
- return [
1256
- event
1257
- ];
1255
+ this.append(DECISION_BATCH, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef: state.contextRef, requestKey, events: [event] }, `booking-copilot:decision-batch:${taskId}:${requestKey}`);
1256
+ return [event];
1258
1257
  });
1259
1258
  return run.immediate();
1260
1259
  }
1260
+ /** Atomically applies planner decisions and records the exact SSE batch before it is sent. */
1261
1261
  applyDecisionBatch(taskId, requestKey, decisions, includeSubmitted = false) {
1262
1262
  assertInternalDecisionKey(requestKey);
1263
1263
  const prior = this.readDecisionBatch(taskId, requestKey);
1264
- if (prior) return prior;
1264
+ if (prior)
1265
+ return prior;
1265
1266
  const current = this.requireTask(taskId);
1266
- if (current.phase === 'terminal' || current.phase === 'error') throw new Error('task_terminal');
1267
+ if (current.phase === 'terminal' || current.phase === 'error')
1268
+ throw new Error('task_terminal');
1267
1269
  assertReplayUpgradeReanchored(current);
1268
1270
  assertDecisionBatchFinality(decisions);
1269
1271
  assertAvailabilityLiveness(current, decisions);
1270
- if (current.availability.recoveryStarted && !current.availability.terminal && decisions.some((decision)=>decision.kind === 'terminal' || decision.kind === 'error')) throw new Error('availability_terminal_policy_owned');
1271
- for (const decision of decisions){
1272
+ if (current.availability.recoveryStarted && !current.availability.terminal && decisions.some((decision) => decision.kind === 'terminal' || decision.kind === 'error'))
1273
+ throw new Error('availability_terminal_policy_owned');
1274
+ for (const decision of decisions) {
1272
1275
  if (decision.kind === 'operation') {
1273
1276
  const checked = validateBookingReadAction(decision.action);
1274
- if (!checked.ok) throw new Error(`invalid_action:${errorText(checked)}`);
1277
+ if (!checked.ok)
1278
+ throw new Error(`invalid_action:${errorText(checked)}`);
1275
1279
  }
1276
1280
  }
1277
- const run = this.ledger.db.transaction(()=>{
1281
+ const run = this.ledger.db.transaction(() => {
1278
1282
  const events = [];
1279
1283
  const before = this.requireTask(taskId);
1280
1284
  assertReplayUpgradeReanchored(before);
1281
1285
  assertAvailabilityLiveness(before, decisions);
1282
- if (before.availability.recoveryStarted && !before.availability.terminal && decisions.some((decision)=>decision.kind === 'terminal' || decision.kind === 'error')) throw new Error('availability_terminal_policy_owned');
1283
- for (const decision of decisions){
1284
- if (decision.kind !== 'question') continue;
1286
+ if (before.availability.recoveryStarted && !before.availability.terminal && decisions.some((decision) => decision.kind === 'terminal' || decision.kind === 'error'))
1287
+ throw new Error('availability_terminal_policy_owned');
1288
+ for (const decision of decisions) {
1289
+ if (decision.kind !== 'question')
1290
+ continue;
1285
1291
  const awaiting = before.awaitingApproval;
1286
- if (!awaiting || requestKey !== awaiting.presentationRequestKey || bookingDigest(decision.question.blocker) !== bookingDigest(awaiting.blocker) || decision.question.approvalOptions.length !== awaiting.options.length || decision.question.approvalOptions.some(({ approval })=>!awaiting.options.some((option)=>option.optionDigest === approval.optionDigest && bookingDigest(option) === bookingDigest(approval)))) throw new Error('approval_presentation_key_mismatch');
1292
+ if (!awaiting || requestKey !== awaiting.presentationRequestKey || bookingDigest(decision.question.blocker) !== bookingDigest(awaiting.blocker) || decision.question.approvalOptions.length !== awaiting.options.length || decision.question.approvalOptions.some(({ approval }) => !awaiting.options.some((option) => option.optionDigest === approval.optionDigest && bookingDigest(option) === bookingDigest(approval))))
1293
+ throw new Error('approval_presentation_key_mismatch');
1287
1294
  this.ensureApprovalOffered(taskId, before.contextRef, awaiting, requestKey);
1288
1295
  }
1289
- if (includeSubmitted) events.push(this.emitEventInTransaction(taskId, {
1290
- kind: 'status',
1291
- status: 'submitted'
1292
- }));
1293
- events.push(this.emitEventInTransaction(taskId, {
1294
- kind: 'status',
1295
- status: 'working'
1296
- }));
1297
- for (const decision of decisions){
1296
+ if (includeSubmitted)
1297
+ events.push(this.emitEventInTransaction(taskId, { kind: 'status', status: 'submitted' }));
1298
+ events.push(this.emitEventInTransaction(taskId, { kind: 'status', status: 'working' }));
1299
+ for (const decision of decisions) {
1298
1300
  if (decision.kind === 'operation') {
1299
1301
  events.push(this.issueOperationInTransaction(taskId, decision.action));
1300
- events.push(this.emitEventInTransaction(taskId, {
1301
- kind: 'status',
1302
- status: 'waiting_receipt'
1303
- }));
1304
- } else events.push(this.emitEventInTransaction(taskId, decision));
1302
+ events.push(this.emitEventInTransaction(taskId, { kind: 'status', status: 'waiting_receipt' }));
1303
+ }
1304
+ else
1305
+ events.push(this.emitEventInTransaction(taskId, decision));
1305
1306
  }
1306
1307
  const state = this.requireTask(taskId);
1307
- this.append(DECISION_BATCH, taskId, {
1308
- schema: LEDGER_SCHEMA,
1309
- taskId,
1310
- contextRef: state.contextRef,
1311
- requestKey,
1312
- events
1313
- }, `booking-copilot:decision-batch:${taskId}:${requestKey}`);
1308
+ this.append(DECISION_BATCH, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef: state.contextRef, requestKey, events }, `booking-copilot:decision-batch:${taskId}:${requestKey}`);
1314
1309
  return events;
1315
1310
  });
1316
1311
  return run.immediate();
1317
1312
  }
1318
1313
  issueOperation(taskId, candidate) {
1319
1314
  const validation = validateBookingReadAction(candidate);
1320
- if (!validation.ok) throw new Error(`invalid_action:${errorText(validation)}`);
1315
+ if (!validation.ok)
1316
+ throw new Error(`invalid_action:${errorText(validation)}`);
1321
1317
  assertTaskId(taskId);
1322
1318
  assertSafeRef(candidate.actionId);
1323
1319
  candidate.factRefs.forEach(assertSafeRef);
1324
- return this.ledger.db.transaction(()=>this.issueOperationInTransaction(taskId, candidate)).immediate();
1320
+ return this.ledger.db.transaction(() => this.issueOperationInTransaction(taskId, candidate)).immediate();
1325
1321
  }
1326
1322
  issueOperationInTransaction(taskId, candidate) {
1327
1323
  assertSafeRef(candidate.actionId);
1328
1324
  candidate.factRefs.forEach(assertSafeRef);
1329
1325
  const state = this.requireTask(taskId);
1330
- if (state.phase === 'terminal' || state.phase === 'error') throw new Error('task_terminal');
1326
+ if (state.phase === 'terminal' || state.phase === 'error')
1327
+ throw new Error('task_terminal');
1331
1328
  assertReplayUpgradeReanchored(state);
1332
1329
  if (state.pendingAction) {
1333
- if (state.pendingAction.actionId !== candidate.actionId) throw new Error('receipt_required');
1334
- if (!matchesCheckpoint(candidate, state.pendingAction)) throw new Error('action_conflict');
1330
+ if (state.pendingAction.actionId !== candidate.actionId)
1331
+ throw new Error('receipt_required');
1332
+ if (!matchesCheckpoint(candidate, state.pendingAction))
1333
+ throw new Error('action_conflict');
1335
1334
  return this.operationEvent(taskId, state.pendingAction, candidate);
1336
1335
  }
1337
- if (candidate.contextRef !== state.contextRef) throw new Error('context_mismatch');
1338
- if (!state.allowedActions.includes(candidate.kind)) throw new Error('unsupported_action');
1339
- if (candidate.expectedRevision !== state.revision) throw new Error('stale_revision');
1340
- if (state.operationCount >= BOOKING_COPILOT_MAX_OPERATIONS) throw new Error('operation_limit_reached');
1341
- if (candidate.relaxationApprovalRef) throw new Error('approval_ref_planner_owned_forbidden');
1342
- if (!state.workspaceSnapshot) throw new Error(`ledger_corrupt:${taskId}:workspace_missing`);
1343
- if (!actionHitsCurrentOfferVersion(state.workspaceSnapshot, candidate, this.now())) throw new Error('offer_version_not_loaded');
1336
+ if (candidate.contextRef !== state.contextRef)
1337
+ throw new Error('context_mismatch');
1338
+ if (!state.allowedActions.includes(candidate.kind))
1339
+ throw new Error('unsupported_action');
1340
+ if (candidate.expectedRevision !== state.revision)
1341
+ throw new Error('stale_revision');
1342
+ if (state.operationCount >= BOOKING_COPILOT_MAX_OPERATIONS)
1343
+ throw new Error('operation_limit_reached');
1344
+ if (candidate.relaxationApprovalRef)
1345
+ throw new Error('approval_ref_planner_owned_forbidden');
1346
+ if (!state.workspaceSnapshot)
1347
+ throw new Error(`ledger_corrupt:${taskId}:workspace_missing`);
1348
+ if (!actionHitsCurrentOfferVersion(state.workspaceSnapshot, candidate, this.now()))
1349
+ throw new Error('offer_version_not_loaded');
1344
1350
  let action = candidate;
1345
1351
  const availability = reduceAvailabilityAction(state.availability, state.workspaceSnapshot, candidate);
1346
1352
  if (state.awaitingApproval) {
1347
1353
  const approval = state.awaitingApproval;
1348
- if (!approval.approval || Date.parse(approval.expiresAt) <= Date.parse(this.now())) throw new Error('approval_expired');
1354
+ if (!approval.approval || Date.parse(approval.expiresAt) <= Date.parse(this.now()))
1355
+ throw new Error('approval_expired');
1349
1356
  const ref = {
1350
- approvalId: approval.approval.approvalId,
1351
- blockerId: approval.blocker.blockerId,
1352
- contextRef: state.contextRef,
1353
- sourceTurnId: approval.sourceTurnId,
1354
- presentationRequestKey: approval.presentationRequestKey,
1355
- sourceActionId: approval.blocker.sourceActionId,
1356
- targetActionId: candidate.actionId,
1357
- sourceRevision: candidate.expectedRevision,
1358
- targetActionKind: candidate.kind,
1359
- to: approval.approval.to,
1360
- expiresAt: approval.expiresAt,
1361
- nonce: approval.nonce,
1362
- sourceReceiptDigest: approval.blocker.sourceReceiptDigest,
1363
- scope: approval.blocker.scope,
1364
- code: approval.blocker.code,
1365
- criterionPath: approval.blocker.criterionPath,
1366
- valueDigest: approval.blocker.valueDigest
1357
+ approvalId: approval.approval.approvalId, blockerId: approval.blocker.blockerId,
1358
+ contextRef: state.contextRef, sourceTurnId: approval.sourceTurnId,
1359
+ presentationRequestKey: approval.presentationRequestKey, sourceActionId: approval.blocker.sourceActionId,
1360
+ targetActionId: candidate.actionId, sourceRevision: candidate.expectedRevision,
1361
+ targetActionKind: candidate.kind, to: approval.approval.to, expiresAt: approval.expiresAt,
1362
+ nonce: approval.nonce, sourceReceiptDigest: approval.blocker.sourceReceiptDigest,
1363
+ scope: approval.blocker.scope, code: approval.blocker.code, criterionPath: approval.blocker.criterionPath,
1364
+ valueDigest: approval.blocker.valueDigest,
1367
1365
  };
1368
- action = {
1369
- ...candidate,
1370
- relaxationApprovalRef: ref
1371
- };
1372
- this.append(APPROVAL_CONSUMED, taskId, {
1373
- schema: LEDGER_SCHEMA,
1374
- taskId,
1375
- contextRef: state.contextRef,
1376
- approval,
1377
- ref,
1378
- approvalDigest: bookingDigest(ref)
1379
- }, `booking-copilot:approval-consumed:${taskId}:${approval.approval.approvalId}:${approval.nonce}`);
1366
+ action = { ...candidate, relaxationApprovalRef: ref };
1367
+ this.append(APPROVAL_CONSUMED, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef: state.contextRef, approval, ref, approvalDigest: bookingDigest(ref) }, `booking-copilot:approval-consumed:${taskId}:${approval.approval.approvalId}:${approval.nonce}`);
1380
1368
  }
1381
1369
  const eventId = this.idFactory('event');
1382
1370
  const emittedAt = this.now();
1383
1371
  const sequence = state.lastSequence + 1;
1384
- const event = {
1385
- schemaVersion: 'booking.surface',
1386
- eventId,
1387
- taskId,
1388
- contextRef: state.contextRef,
1389
- sequence,
1390
- emittedAt,
1391
- kind: 'operation',
1392
- action
1393
- };
1372
+ const event = { schemaVersion: 'booking.surface', eventId, taskId, contextRef: state.contextRef, sequence, emittedAt, kind: 'operation', action };
1394
1373
  const eventValidation = validateBookingSurfaceEvent(event);
1395
- if (!eventValidation.ok) throw new Error(`invalid_operation_event:${errorText(eventValidation)}`);
1396
- if (!state.lastTurnId) throw new Error(`ledger_corrupt:${taskId}:turn_id`);
1374
+ if (!eventValidation.ok)
1375
+ throw new Error(`invalid_operation_event:${errorText(eventValidation)}`);
1376
+ if (!state.lastTurnId)
1377
+ throw new Error(`ledger_corrupt:${taskId}:turn_id`);
1397
1378
  this.appendAction(taskId, state.contextRef, checkpointFor(action, eventId, sequence, emittedAt, state.lastTurnId), state.operationCount + 1, availability);
1398
1379
  return event;
1399
1380
  }
1400
1381
  continueWithReceipt(turn) {
1401
1382
  const validation = validateBookingSurface(turn);
1402
- if (!validation.ok) throw new Error(`invalid_receipt_continuation:${errorText(validation)}`);
1383
+ if (!validation.ok)
1384
+ throw new Error(`invalid_receipt_continuation:${errorText(validation)}`);
1403
1385
  assertWorkspaceLoadedOfferRefsUnique(turn.workspace);
1404
1386
  assertCanonicalReceipt(turn.receipt);
1405
- const run = this.ledger.db.transaction(()=>{
1387
+ const run = this.ledger.db.transaction(() => {
1406
1388
  const state = this.requireTask(turn.taskId);
1407
1389
  assertReplayUpgradeReanchored(state);
1408
- if (turn.workspace.contextRef !== state.contextRef || turn.receipt.contextRef !== state.contextRef) throw new Error('context_mismatch');
1390
+ if (turn.workspace.contextRef !== state.contextRef || turn.receipt.contextRef !== state.contextRef)
1391
+ throw new Error('context_mismatch');
1409
1392
  const receiptWorkspace = bookingWorkspaceDigest(turn.workspace);
1410
1393
  const receiptWorkspaceSemantic = bookingWorkspaceSemanticDigest(turn.workspace);
1411
- if (!state.workspaceSnapshot || !workspaceBoundaryMatches(state.workspaceSnapshot, turn.workspace)) throw new Error('workspace_mismatch');
1394
+ if (!state.workspaceSnapshot || !workspaceBoundaryMatches(state.workspaceSnapshot, turn.workspace))
1395
+ throw new Error('workspace_mismatch');
1412
1396
  const existing = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ? ORDER BY seq`).all(this.ledger.tenant, turn.taskId, RECEIPT);
1413
1397
  const receiptDigest = canonicalReceiptDigest(turn.receipt);
1414
- if (state.legacySuppressedReceiptActionIds?.includes(turn.receipt.actionId)) throw new Error('request_key_invalidated');
1415
- for (const row of existing){
1398
+ if (state.legacySuppressedReceiptActionIds?.includes(turn.receipt.actionId))
1399
+ throw new Error('request_key_invalidated');
1400
+ for (const row of existing) {
1416
1401
  const prior = JSON.parse(row.payload);
1417
- if (prior.receipt.actionId !== turn.receipt.actionId) continue;
1418
- if (prior.receiptDigest !== receiptDigest) throw new Error('receipt_conflict');
1419
- if (prior.workspaceDigest !== receiptWorkspace) throw new Error('workspace_mismatch');
1402
+ if (prior.receipt.actionId !== turn.receipt.actionId)
1403
+ continue;
1404
+ if (prior.receiptDigest !== receiptDigest)
1405
+ throw new Error('receipt_conflict');
1406
+ if (prior.workspaceDigest !== receiptWorkspace)
1407
+ throw new Error('workspace_mismatch');
1420
1408
  return state;
1421
1409
  }
1422
- if (state.phase === 'terminal' || state.phase === 'error') throw new Error('task_terminal');
1423
- if (!state.pendingAction) throw new Error('unexpected_receipt');
1424
- if (state.pendingAction.actionId !== turn.receipt.actionId) throw new Error('receipt_action_mismatch');
1425
- if (turn.receipt.revision < state.pendingAction.expectedRevision) throw new Error('revision_regression');
1426
- if (turn.receipt.resultContract.blockers.some((blocker)=>blocker.sourceActionId !== state.pendingAction.actionId)) throw new Error('receipt_source_action_mismatch');
1427
- if (!workspacePostActionMatches(state.workspaceSnapshot, turn.workspace, state.pendingAction, turn.receipt)) throw new Error('workspace_mismatch');
1428
- if (!receiptObservationMatchesAction(turn.receipt.observation.kind, state.pendingAction.kind)) throw new Error('receipt_observation_action_mismatch');
1429
- if (!receiptTargetMatchesAction(turn.receipt, state.pendingAction)) throw new Error('receipt_target_mismatch');
1430
- if (state.pendingAction.kind === 'offer.check' && turn.receipt.status === 'applied' && !verifiedOfferUnexpired(turn.workspace, this.now())) throw new Error('receipt_verified_offer_expired');
1431
- if (state.pendingAction.kind === 'offers.query' && turn.receipt.observation.kind === 'offers.state') validateOffersReceiptWorkspace(turn.receipt, turn.workspace, state.pendingAction.input.hotelRefs);
1432
- if (turn.receipt.resultContract.relaxationsApplied.some((approval)=>!this.approvalWasConsumed(turn.taskId, approval, state.pendingAction.actionId))) throw new Error('receipt_relaxation_unauthorized');
1410
+ if (state.phase === 'terminal' || state.phase === 'error')
1411
+ throw new Error('task_terminal');
1412
+ if (!state.pendingAction)
1413
+ throw new Error('unexpected_receipt');
1414
+ if (state.pendingAction.actionId !== turn.receipt.actionId)
1415
+ throw new Error('receipt_action_mismatch');
1416
+ if (turn.receipt.revision < state.pendingAction.expectedRevision)
1417
+ throw new Error('revision_regression');
1418
+ if (turn.receipt.resultContract.blockers.some((blocker) => blocker.sourceActionId !== state.pendingAction.actionId))
1419
+ throw new Error('receipt_source_action_mismatch');
1420
+ if (!workspacePostActionMatches(state.workspaceSnapshot, turn.workspace, state.pendingAction, turn.receipt))
1421
+ throw new Error('workspace_mismatch');
1422
+ if (!receiptObservationMatchesAction(turn.receipt.observation.kind, state.pendingAction.kind))
1423
+ throw new Error('receipt_observation_action_mismatch');
1424
+ if (!receiptTargetMatchesAction(turn.receipt, state.pendingAction))
1425
+ throw new Error('receipt_target_mismatch');
1426
+ if (state.pendingAction.kind === 'offer.check' && turn.receipt.status === 'applied' && !verifiedOfferUnexpired(turn.workspace, this.now()))
1427
+ throw new Error('receipt_verified_offer_expired');
1428
+ // Failed/stale query receipts may carry a typed gap observation; only an
1429
+ // offers.state observation can claim passive offer provenance.
1430
+ if (state.pendingAction.kind === 'offers.query' && turn.receipt.observation.kind === 'offers.state')
1431
+ validateOffersReceiptWorkspace(turn.receipt, turn.workspace, state.pendingAction.input.hotelRefs);
1432
+ if (turn.receipt.resultContract.relaxationsApplied.some((approval) => !this.approvalWasConsumed(turn.taskId, approval, state.pendingAction.actionId)))
1433
+ throw new Error('receipt_relaxation_unauthorized');
1433
1434
  const pending = state.pendingAction;
1434
- if (turn.receipt.revision !== turn.workspace.revision) throw new Error('revision_mismatch');
1435
+ if (turn.receipt.revision !== turn.workspace.revision)
1436
+ throw new Error('revision_mismatch');
1435
1437
  const availability = reduceAvailabilityReceipt(state.availability, turn.workspace, turn.receipt, pending);
1436
1438
  const availabilityTerminal = availabilityPolicyTerminal(availability);
1437
- this.append(RECEIPT, turn.taskId, {
1438
- schema: LEDGER_SCHEMA,
1439
- taskId: turn.taskId,
1440
- contextRef: state.contextRef,
1441
- receipt: turn.receipt,
1442
- receiptDigest,
1443
- operationCount: state.operationCount,
1444
- workspaceDigest: receiptWorkspace,
1445
- workspaceSemanticDigest: receiptWorkspaceSemantic,
1446
- workspace: turn.workspace,
1447
- availability,
1448
- availabilityDigest: bookingDigest(availability),
1449
- ...availabilityTerminal ? {
1450
- availabilityTerminal
1451
- } : {}
1452
- }, `booking-copilot:receipt:${turn.taskId}:${turn.receipt.actionId}`);
1439
+ this.append(RECEIPT, turn.taskId, { schema: LEDGER_SCHEMA, taskId: turn.taskId, contextRef: state.contextRef, receipt: turn.receipt, receiptDigest, operationCount: state.operationCount, workspaceDigest: receiptWorkspace, workspaceSemanticDigest: receiptWorkspaceSemantic, workspace: turn.workspace, availability, availabilityDigest: bookingDigest(availability), ...(availabilityTerminal ? { availabilityTerminal } : {}) }, `booking-copilot:receipt:${turn.taskId}:${turn.receipt.actionId}`);
1453
1440
  if (availabilityTerminal || state.operationCount >= BOOKING_COPILOT_MAX_OPERATIONS) {
1454
1441
  const terminalState = this.requireTask(turn.taskId);
1455
1442
  const terminalEvent = this.terminalEvent(terminalState);
1456
1443
  this.appendTerminalEvent(turn.taskId, terminalEvent);
1457
- this.append(DECISION_BATCH, turn.taskId, {
1458
- schema: LEDGER_SCHEMA,
1459
- taskId: turn.taskId,
1460
- contextRef: state.contextRef,
1461
- requestKey: `receipt:${turn.receipt.actionId}:${receiptDigest}`,
1462
- events: [
1463
- terminalEvent
1464
- ]
1465
- }, `booking-copilot:decision-batch:${turn.taskId}:receipt:${turn.receipt.actionId}:${receiptDigest}`);
1444
+ this.append(DECISION_BATCH, turn.taskId, { schema: LEDGER_SCHEMA, taskId: turn.taskId, contextRef: state.contextRef, requestKey: `receipt:${turn.receipt.actionId}:${receiptDigest}`, events: [terminalEvent] }, `booking-copilot:decision-batch:${turn.taskId}:receipt:${turn.receipt.actionId}:${receiptDigest}`);
1466
1445
  return terminalState;
1467
1446
  }
1468
1447
  const blocker = turn.receipt.resultContract.blockers[0];
1469
1448
  if (blocker) {
1449
+ // Never derive this claim from the injectable id factory: production
1450
+ // and crash proofs must not make a presentation nonce guessable from
1451
+ // deterministic task/event ids.
1470
1452
  const nonce = randomUUID();
1471
1453
  const sourceTurnId = state.pendingAction?.sourceTurnId ?? state.lastTurnId ?? `ordinal:${state.userTurnCount}`;
1472
1454
  const requestKey = presentationKey(turn.taskId, sourceTurnId, turn.receipt.actionId, receiptDigest);
1473
- const approval = {
1474
- blocker,
1475
- options: [
1476
- approvalOption(turn.taskId, state.contextRef, sourceTurnId, requestKey, blocker, 'prefer', nonce),
1477
- approvalOption(turn.taskId, state.contextRef, sourceTurnId, requestKey, blocker, 'drop', nonce)
1478
- ],
1479
- optionsEmitted: false,
1480
- approval: undefined,
1481
- nonce,
1482
- expiresAt: new Date(Date.parse(this.now()) + this.approvalTtlMs).toISOString(),
1483
- sourceTurnId,
1484
- presentationRequestKey: requestKey
1485
- };
1486
- this.append(APPROVAL_GRANTED, turn.taskId, {
1487
- schema: LEDGER_SCHEMA,
1488
- taskId: turn.taskId,
1489
- contextRef: state.contextRef,
1490
- approval,
1491
- approvalDigest: bookingDigest(approval)
1492
- }, `booking-copilot:approval-awaiting:${turn.taskId}:${turn.receipt.actionId}`);
1455
+ const approval = { blocker, options: [approvalOption(turn.taskId, state.contextRef, sourceTurnId, requestKey, blocker, 'prefer', nonce), approvalOption(turn.taskId, state.contextRef, sourceTurnId, requestKey, blocker, 'drop', nonce)], optionsEmitted: false, approval: undefined, nonce, expiresAt: new Date(Date.parse(this.now()) + this.approvalTtlMs).toISOString(), sourceTurnId, presentationRequestKey: requestKey };
1456
+ this.append(APPROVAL_GRANTED, turn.taskId, { schema: LEDGER_SCHEMA, taskId: turn.taskId, contextRef: state.contextRef, approval, approvalDigest: bookingDigest(approval) }, `booking-copilot:approval-awaiting:${turn.taskId}:${turn.receipt.actionId}`);
1493
1457
  }
1494
1458
  return this.requireTask(turn.taskId);
1495
1459
  });
1496
1460
  return run.immediate();
1497
1461
  }
1498
1462
  emitEvent(taskId, draft) {
1499
- return this.ledger.db.transaction(()=>{
1463
+ return this.ledger.db.transaction(() => {
1500
1464
  const state = this.requireTask(taskId);
1501
- if (state.phase === 'terminal' || state.phase === 'error') throw new Error('task_terminal');
1465
+ if (state.phase === 'terminal' || state.phase === 'error')
1466
+ throw new Error('task_terminal');
1502
1467
  assertReplayUpgradeReanchored(state);
1503
1468
  return this.emitEventInTransaction(taskId, draft);
1504
1469
  }).immediate();
@@ -1506,89 +1471,40 @@ export class BookingCopilotTaskRuntime {
1506
1471
  emitEventInTransaction(taskId, draft) {
1507
1472
  const state = this.requireTask(taskId);
1508
1473
  assertReplayUpgradeReanchored(state);
1509
- const safeDraft = draft.kind === 'error' ? {
1510
- ...draft,
1511
- error: {
1512
- ...draft.error,
1513
- code: normalizeBookingErrorCode(draft.error.code),
1514
- message: safeBookingErrorMessage(normalizeBookingErrorCode(draft.error.code))
1515
- }
1516
- } : draft;
1517
- const event = {
1518
- schemaVersion: 'booking.surface',
1519
- eventId: this.idFactory('event'),
1520
- taskId,
1521
- contextRef: state.contextRef,
1522
- sequence: state.lastSequence + 1,
1523
- emittedAt: this.now(),
1524
- ...safeDraft
1525
- };
1474
+ const safeDraft = draft.kind === 'error'
1475
+ ? { ...draft, error: { ...draft.error, code: normalizeBookingErrorCode(draft.error.code), message: safeBookingErrorMessage(normalizeBookingErrorCode(draft.error.code)) } }
1476
+ : draft;
1477
+ const event = { schemaVersion: 'booking.surface', eventId: this.idFactory('event'), taskId, contextRef: state.contextRef, sequence: state.lastSequence + 1, emittedAt: this.now(), ...safeDraft };
1526
1478
  const checked = validateBookingSurfaceEvent(event);
1527
- if (!checked.ok) throw new Error(`invalid_event:${errorText(checked)}`);
1528
- this.append(EVENT, taskId, {
1529
- schema: LEDGER_SCHEMA,
1530
- taskId,
1531
- contextRef: state.contextRef,
1532
- eventId: event.eventId,
1533
- sequence: event.sequence,
1534
- emittedAt: event.emittedAt,
1535
- eventKind: event.kind,
1536
- ...event.kind === 'status' ? {
1537
- status: event.status
1538
- } : {},
1539
- contentDigest: bookingDigest(safeDraft)
1540
- }, `booking-copilot:event:${taskId}:${event.eventId}`);
1479
+ if (!checked.ok)
1480
+ throw new Error(`invalid_event:${errorText(checked)}`);
1481
+ this.append(EVENT, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef: state.contextRef, eventId: event.eventId, sequence: event.sequence, emittedAt: event.emittedAt, eventKind: event.kind, ...(event.kind === 'status' ? { status: event.status } : {}), contentDigest: bookingDigest(safeDraft) }, `booking-copilot:event:${taskId}:${event.eventId}`);
1541
1482
  return event;
1542
1483
  }
1543
1484
  operationEvent(taskId, checkpoint, action) {
1544
- const replayAction = action.relaxationApprovalRef || !checkpoint.relaxationApprovalRef ? action : {
1545
- ...action,
1546
- relaxationApprovalRef: checkpoint.relaxationApprovalRef
1547
- };
1548
- return {
1549
- schemaVersion: 'booking.surface',
1550
- eventId: checkpoint.eventId,
1551
- taskId,
1552
- contextRef: checkpoint.contextRef,
1553
- sequence: checkpoint.sequence,
1554
- emittedAt: checkpoint.emittedAt,
1555
- kind: 'operation',
1556
- action: replayAction
1557
- };
1485
+ const replayAction = action.relaxationApprovalRef || !checkpoint.relaxationApprovalRef ? action : { ...action, relaxationApprovalRef: checkpoint.relaxationApprovalRef };
1486
+ return { schemaVersion: 'booking.surface', eventId: checkpoint.eventId, taskId, contextRef: checkpoint.contextRef, sequence: checkpoint.sequence, emittedAt: checkpoint.emittedAt, kind: 'operation', action: replayAction };
1558
1487
  }
1559
1488
  terminalEvent(state) {
1560
- if (!taskHasTerminalBudget(state)) throw new Error('task_not_terminal');
1489
+ if (!taskHasTerminalBudget(state))
1490
+ throw new Error('task_not_terminal');
1561
1491
  const terminalCode = state.availability.terminal?.code ?? 'operation_limit_reached';
1562
- const event = {
1563
- schemaVersion: 'booking.surface',
1564
- eventId: `terminal-${state.taskId}-${state.operationCount}`,
1565
- taskId: state.taskId,
1566
- contextRef: state.contextRef,
1567
- sequence: state.lastSequence + 1,
1568
- emittedAt: this.now(),
1569
- kind: 'terminal',
1570
- terminal: {
1571
- status: state.availability.terminal?.code === 'availability_confirmed' ? 'completed' : 'stopped',
1572
- summary: terminalCode,
1573
- factRefs: []
1574
- }
1575
- };
1492
+ const event = { schemaVersion: 'booking.surface', eventId: `terminal-${state.taskId}-${state.operationCount}`, taskId: state.taskId, contextRef: state.contextRef, sequence: state.lastSequence + 1, emittedAt: this.now(), kind: 'terminal', terminal: { status: state.availability.terminal?.code === 'availability_confirmed' ? 'completed' : 'stopped', summary: terminalCode, factRefs: [] } };
1576
1493
  const checked = validateBookingSurfaceEvent(event);
1577
- if (!checked.ok) throw new Error(`invalid_terminal_event:${errorText(checked)}`);
1494
+ if (!checked.ok)
1495
+ throw new Error(`invalid_terminal_event:${errorText(checked)}`);
1578
1496
  return event;
1579
1497
  }
1580
- requireTask(taskId) {
1581
- const state = this.resumeTask(taskId);
1582
- if (!state) throw new Error('task_not_found');
1583
- return state;
1584
- }
1498
+ requireTask(taskId) { const state = this.resumeTask(taskId); if (!state)
1499
+ throw new Error('task_not_found'); return state; }
1585
1500
  requestBinding(requestKey) {
1586
1501
  const rows = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND kind = ? ORDER BY seq DESC`).all(this.ledger.tenant, REQUEST_BINDING);
1587
- for (const row of rows){
1502
+ for (const row of rows) {
1588
1503
  const binding = JSON.parse(row.payload);
1589
1504
  if (binding.requestKey === requestKey) {
1590
1505
  const state = this.resumeTask(binding.taskId);
1591
- if (state?.legacySuppressedRequestBindingKeys?.includes(requestKey)) throw new Error('request_key_invalidated');
1506
+ if (state?.legacySuppressedRequestBindingKeys?.includes(requestKey))
1507
+ throw new Error('request_key_invalidated');
1592
1508
  return binding;
1593
1509
  }
1594
1510
  }
@@ -1597,7 +1513,8 @@ export class BookingCopilotTaskRuntime {
1597
1513
  requestBindingPayload(requestKey, turn, input) {
1598
1514
  assertPrincipal(input.principal);
1599
1515
  assertRequestKey(requestKey);
1600
- if (input.taskHandle) assertSafeRef(input.taskHandle);
1516
+ if (input.taskHandle)
1517
+ assertSafeRef(input.taskHandle);
1601
1518
  return {
1602
1519
  requestKey,
1603
1520
  requestDigest: bookingTurnDigest(turn),
@@ -1606,15 +1523,9 @@ export class BookingCopilotTaskRuntime {
1606
1523
  workspaceSemanticDigest: bookingWorkspaceSemanticDigest(turn.workspace),
1607
1524
  surface: turn.workspace.surface,
1608
1525
  capabilityDigest: bookingDigest(turn.workspace.capabilities),
1609
- principalDigest: bookingDigest({
1610
- subject: input.principal.subject
1611
- }),
1612
- scopeDigest: bookingDigest({
1613
- scope: input.principal.scope
1614
- }),
1615
- ...input.taskHandle ? {
1616
- taskHandle: input.taskHandle
1617
- } : {}
1526
+ principalDigest: bookingDigest({ subject: input.principal.subject }),
1527
+ scopeDigest: bookingDigest({ scope: input.principal.scope }),
1528
+ ...(input.taskHandle ? { taskHandle: input.taskHandle } : {}),
1618
1529
  };
1619
1530
  }
1620
1531
  appendRequestBindingInTransaction(requestKey, turn, input) {
@@ -1623,164 +1534,127 @@ export class BookingCopilotTaskRuntime {
1623
1534
  const payload = this.requestBindingPayload(requestKey, turn, input);
1624
1535
  if (input.taskHandle) {
1625
1536
  const prior = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND kind = ?`).all(this.ledger.tenant, REQUEST_BINDING);
1626
- for (const row of prior){
1537
+ for (const row of prior) {
1627
1538
  const binding = JSON.parse(row.payload);
1628
- if (binding.taskHandle !== input.taskHandle) continue;
1629
- if (binding.principalDigest !== payload.principalDigest || binding.scopeDigest !== payload.scopeDigest) throw new Error('principal_conflict');
1630
- if (binding.taskId !== turn.taskId || binding.contextRef !== turn.workspace.contextRef) throw new Error('task_handle_conflict');
1539
+ if (binding.taskHandle !== input.taskHandle)
1540
+ continue;
1541
+ if (binding.principalDigest !== payload.principalDigest || binding.scopeDigest !== payload.scopeDigest)
1542
+ throw new Error('principal_conflict');
1543
+ if (binding.taskId !== turn.taskId || binding.contextRef !== turn.workspace.contextRef)
1544
+ throw new Error('task_handle_conflict');
1631
1545
  }
1632
1546
  }
1633
1547
  const existing = this.requestBinding(requestKey);
1634
1548
  if (existing) {
1635
- if (existing.taskId !== turn.taskId || existing.contextRef !== turn.workspace.contextRef || existing.requestDigest !== payload.requestDigest || existing.turnId !== payload.turnId || existing.workspaceDigest !== payload.workspaceDigest || existing.workspaceSemanticDigest !== payload.workspaceSemanticDigest || existing.surface !== payload.surface || existing.capabilityDigest !== payload.capabilityDigest || existing.principalDigest !== payload.principalDigest || existing.scopeDigest !== payload.scopeDigest || existing.taskHandle !== payload.taskHandle) throw new Error('request_conflict');
1549
+ if (existing.taskId !== turn.taskId || existing.contextRef !== turn.workspace.contextRef || existing.requestDigest !== payload.requestDigest || existing.turnId !== payload.turnId || existing.workspaceDigest !== payload.workspaceDigest || existing.workspaceSemanticDigest !== payload.workspaceSemanticDigest || existing.surface !== payload.surface || existing.capabilityDigest !== payload.capabilityDigest || existing.principalDigest !== payload.principalDigest || existing.scopeDigest !== payload.scopeDigest || existing.taskHandle !== payload.taskHandle)
1550
+ throw new Error('request_conflict');
1636
1551
  return;
1637
1552
  }
1638
- this.append(REQUEST_BINDING, turn.taskId, {
1639
- schema: LEDGER_SCHEMA,
1640
- taskId: turn.taskId,
1641
- contextRef: turn.workspace.contextRef,
1642
- ...payload
1643
- }, `booking-copilot:request-binding:${requestKey}`);
1553
+ this.append(REQUEST_BINDING, turn.taskId, { schema: LEDGER_SCHEMA, taskId: turn.taskId, contextRef: turn.workspace.contextRef, ...payload }, `booking-copilot:request-binding:${requestKey}`);
1644
1554
  }
1645
1555
  normalizeDecisionBatchFromDurableRows(taskId, batchEvents, batchSeq) {
1646
1556
  const individualRows = this.ledger.db.prepare(`SELECT kind, payload FROM events WHERE tenant_id = ? AND run_id = ? AND seq < ? AND kind IN (?, ?) ORDER BY seq`).all(this.ledger.tenant, taskId, batchSeq, EVENT, ACTION);
1647
1557
  const turnRows = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ? AND seq < ?`).all(this.ledger.tenant, taskId, TURN, batchSeq);
1648
1558
  const sourceWorkspaces = new Map();
1649
- for (const row of turnRows){
1559
+ for (const row of turnRows) {
1650
1560
  try {
1651
1561
  const turn = JSON.parse(row.payload);
1652
1562
  sourceWorkspaces.set(turn.turnId, normalizeWorkspaceForReplay(turn.workspace));
1653
- } catch {
1563
+ }
1564
+ catch {
1654
1565
  return null;
1655
1566
  }
1656
1567
  }
1657
1568
  const recordedEvents = new Map();
1658
1569
  const recordedActions = new Map();
1659
- for (const row of individualRows){
1570
+ for (const row of individualRows) {
1660
1571
  if (row.kind === EVENT) {
1661
1572
  const payload = JSON.parse(row.payload);
1662
1573
  const key = consumedIdentityKey(payload.eventId, payload.sequence);
1663
- if (recordedEvents.has(key) || recordedActions.has(key)) return null;
1574
+ if (recordedEvents.has(key) || recordedActions.has(key))
1575
+ return null;
1664
1576
  recordedEvents.set(key, payload);
1665
- } else if (row.kind === ACTION) {
1577
+ }
1578
+ else if (row.kind === ACTION) {
1666
1579
  const payload = JSON.parse(row.payload);
1667
1580
  let action = payload.action;
1668
1581
  try {
1669
1582
  action = normalizeActionCheckpointForReplay(action, sourceWorkspaces.get(action.sourceTurnId));
1670
- } catch {
1583
+ }
1584
+ catch {
1671
1585
  return null;
1672
1586
  }
1673
1587
  const key = consumedIdentityKey(action.eventId, action.sequence);
1674
- if (recordedEvents.has(key) || recordedActions.has(key)) return null;
1588
+ if (recordedEvents.has(key) || recordedActions.has(key))
1589
+ return null;
1675
1590
  recordedActions.set(key, action);
1676
1591
  }
1677
1592
  }
1678
1593
  let lastBatchSequence = -1;
1679
1594
  const seen = new Set();
1680
1595
  const normalizedEvents = [];
1681
- for (const event of batchEvents){
1682
- if (!Number.isSafeInteger(event.sequence) || event.sequence <= lastBatchSequence) return null;
1596
+ for (const event of batchEvents) {
1597
+ if (!Number.isSafeInteger(event.sequence) || event.sequence <= lastBatchSequence)
1598
+ return null;
1683
1599
  lastBatchSequence = event.sequence;
1684
1600
  const key = consumedIdentityKey(event.eventId, event.sequence);
1685
- if (seen.has(key)) return null;
1601
+ if (seen.has(key))
1602
+ return null;
1686
1603
  seen.add(key);
1687
1604
  if (event.kind === 'operation') {
1688
1605
  const checkpoint = recordedActions.get(key);
1689
- if (!checkpoint || !operationEventMatchesAction(event, checkpoint)) return null;
1690
- const normalized = {
1691
- ...event,
1692
- action: actionFromOperationEventForReplay(event, checkpoint)
1693
- };
1694
- if (!validateBookingSurfaceEvent(normalized).ok) return null;
1606
+ if (!checkpoint || !operationEventMatchesAction(event, checkpoint))
1607
+ return null;
1608
+ const normalized = { ...event, action: actionFromOperationEventForReplay(event, checkpoint) };
1609
+ if (!validateBookingSurfaceEvent(normalized).ok)
1610
+ return null;
1695
1611
  normalizedEvents.push(normalized);
1696
- } else {
1612
+ }
1613
+ else {
1697
1614
  const recorded = recordedEvents.get(key);
1698
- if (!recorded || !validateBookingSurfaceEvent(event).ok || !eventPayloadMatchesEvent(recorded, event)) return null;
1615
+ if (!recorded || !validateBookingSurfaceEvent(event).ok || !eventPayloadMatchesEvent(recorded, event))
1616
+ return null;
1699
1617
  normalizedEvents.push(event);
1700
1618
  }
1701
1619
  }
1702
1620
  return normalizedEvents;
1703
1621
  }
1704
- rows(taskId) {
1705
- return this.ledger.db.prepare(`SELECT seq, kind, payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY seq`).all(this.ledger.tenant, taskId, STARTED, TURN, ACTION, RECEIPT, EVENT, APPROVAL_GRANTED, APPROVAL_OFFERED, APPROVAL_CONSUMED, DECISION_BATCH, REQUEST_BINDING);
1706
- }
1622
+ rows(taskId) { return this.ledger.db.prepare(`SELECT seq, kind, payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ORDER BY seq`).all(this.ledger.tenant, taskId, STARTED, TURN, ACTION, RECEIPT, EVENT, APPROVAL_GRANTED, APPROVAL_OFFERED, APPROVAL_CONSUMED, DECISION_BATCH, REQUEST_BINDING); }
1707
1623
  append(kind, taskId, payload, idemKey) {
1708
1624
  const result = this.ledger.db.prepare(`INSERT OR IGNORE INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(this.ledger.tenant, this.now(), ACTOR, kind, taskId, JSON.stringify(payload), idemKey, taskId);
1709
- if (result.changes === 0) throw new Error(`ledger_write_failed:${kind}`);
1710
- }
1711
- appendTurn(taskId, contextRef, requestDigest, workspaceDigest, workspaceSemanticDigest, ordinal, turnId, workspace) {
1712
- if (!workspace) throw new Error('ledger_write_failed:turn_workspace');
1713
- this.append(TURN, taskId, {
1714
- schema: LEDGER_SCHEMA,
1715
- taskId,
1716
- contextRef,
1717
- requestDigest,
1718
- workspaceDigest,
1719
- workspaceSemanticDigest,
1720
- workspace,
1721
- turnId
1722
- }, `booking-copilot:turn:${taskId}:${turnId}`);
1723
- }
1724
- appendApproval(taskId, contextRef, approval) {
1725
- this.append(APPROVAL_GRANTED, taskId, {
1726
- schema: LEDGER_SCHEMA,
1727
- taskId,
1728
- contextRef,
1729
- approval,
1730
- approvalDigest: bookingDigest(approval)
1731
- }, `booking-copilot:approval-granted:${taskId}:${approval.approval?.approvalId ?? 'pending'}:${approval.nonce}`);
1732
- }
1733
- appendAction(taskId, contextRef, action, operationCount, availability) {
1734
- this.append(ACTION, taskId, {
1735
- schema: LEDGER_SCHEMA,
1736
- taskId,
1737
- contextRef,
1738
- action,
1739
- operationCount,
1740
- availability,
1741
- availabilityDigest: bookingDigest(availability)
1742
- }, `booking-copilot:action:${taskId}:${action.actionId}`);
1625
+ if (result.changes === 0)
1626
+ throw new Error(`ledger_write_failed:${kind}`);
1743
1627
  }
1628
+ appendTurn(taskId, contextRef, requestDigest, workspaceDigest, workspaceSemanticDigest, ordinal, turnId, workspace) { if (!workspace)
1629
+ throw new Error('ledger_write_failed:turn_workspace'); this.append(TURN, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef, requestDigest, workspaceDigest, workspaceSemanticDigest, workspace, turnId }, `booking-copilot:turn:${taskId}:${turnId}`); }
1630
+ appendApproval(taskId, contextRef, approval) { this.append(APPROVAL_GRANTED, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef, approval, approvalDigest: bookingDigest(approval) }, `booking-copilot:approval-granted:${taskId}:${approval.approval?.approvalId ?? 'pending'}:${approval.nonce}`); }
1631
+ appendAction(taskId, contextRef, action, operationCount, availability) { this.append(ACTION, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef, action, operationCount, availability, availabilityDigest: bookingDigest(availability) }, `booking-copilot:action:${taskId}:${action.actionId}`); }
1744
1632
  appendTerminalEvent(taskId, event) {
1745
- const payload = {
1746
- schema: LEDGER_SCHEMA,
1747
- taskId,
1748
- contextRef: event.contextRef,
1749
- eventId: event.eventId,
1750
- sequence: event.sequence,
1751
- emittedAt: event.emittedAt,
1752
- eventKind: 'terminal',
1753
- contentDigest: bookingDigest(event.terminal)
1754
- };
1633
+ const payload = { schema: LEDGER_SCHEMA, taskId, contextRef: event.contextRef, eventId: event.eventId, sequence: event.sequence, emittedAt: event.emittedAt, eventKind: 'terminal', contentDigest: bookingDigest(event.terminal) };
1755
1634
  const idemKey = `booking-copilot:terminal-event:${taskId}`;
1756
1635
  const result = this.ledger.db.prepare(`INSERT OR IGNORE INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(this.ledger.tenant, this.now(), ACTOR, EVENT, taskId, JSON.stringify(payload), idemKey, taskId);
1757
1636
  if (result.changes === 0) {
1758
1637
  const existing = this.ledger.db.prepare('SELECT payload FROM events WHERE tenant_id = ? AND idem_key = ?').get(this.ledger.tenant, idemKey);
1759
- if (!existing || JSON.parse(existing.payload).contentDigest !== payload.contentDigest) throw new Error('ledger_write_failed:terminal_event');
1638
+ if (!existing || JSON.parse(existing.payload).contentDigest !== payload.contentDigest)
1639
+ throw new Error('ledger_write_failed:terminal_event');
1760
1640
  }
1761
1641
  }
1762
1642
  ensureApprovalOffered(taskId, contextRef, approval, requestKey) {
1763
- if (requestKey !== approval.presentationRequestKey) throw new Error('approval_presentation_key_mismatch');
1643
+ if (requestKey !== approval.presentationRequestKey)
1644
+ throw new Error('approval_presentation_key_mismatch');
1764
1645
  const idemKey = `booking-copilot:approval-offered:${taskId}:${requestKey}`;
1765
1646
  const exists = this.ledger.db.prepare('SELECT 1 AS present FROM events WHERE tenant_id = ? AND idem_key = ?').get(this.ledger.tenant, idemKey);
1766
- if (!exists) this.append(APPROVAL_OFFERED, taskId, {
1767
- schema: LEDGER_SCHEMA,
1768
- taskId,
1769
- contextRef,
1770
- approval,
1771
- approvalDigest: bookingDigest(approval)
1772
- }, idemKey);
1647
+ if (!exists)
1648
+ this.append(APPROVAL_OFFERED, taskId, { schema: LEDGER_SCHEMA, taskId, contextRef, approval, approvalDigest: bookingDigest(approval) }, idemKey);
1773
1649
  }
1774
1650
  approvalWasConsumed(taskId, approval, targetActionId) {
1775
1651
  const state = this.resumeTask(taskId);
1776
- if (state?.legacySuppressedApprovalTargetActionIds?.includes(targetActionId)) return false;
1652
+ if (state?.legacySuppressedApprovalTargetActionIds?.includes(targetActionId))
1653
+ return false;
1777
1654
  const rows = this.ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ?`).all(this.ledger.tenant, taskId, APPROVAL_CONSUMED);
1778
- return rows.some(({ payload })=>{
1655
+ return rows.some(({ payload }) => {
1779
1656
  const consumed = JSON.parse(payload);
1780
1657
  return consumed.ref?.targetActionId === targetActionId && consumed.approval?.approval && bookingDigest(consumed.approval.approval) === bookingDigest(approval);
1781
1658
  });
1782
1659
  }
1783
1660
  }
1784
-
1785
-
1786
- //# sourceURL=ts/src/booking-surface/runtime.ts