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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (533) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1788 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +161 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +553 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +676 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/provider-detail-sanitize.js +64 -0
  41. package/dist/capabilities/resilience.js +191 -0
  42. package/dist/capabilities/route-provider-conformance.js +341 -0
  43. package/dist/capabilities/session/action-cache.js +50 -44
  44. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  45. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  46. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  47. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  48. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  49. package/dist/capabilities/session/benchmark.js +189 -0
  50. package/dist/capabilities/session/bridge-ledger.js +224 -0
  51. package/dist/capabilities/session/extension-bridge.js +636 -0
  52. package/dist/capabilities/session/extension-channel.js +109 -0
  53. package/dist/capabilities/session/extension-distribution.js +230 -0
  54. package/dist/capabilities/session/extract.js +31 -21
  55. package/dist/capabilities/session/golden-score.js +114 -0
  56. package/dist/capabilities/session/health-watch.js +215 -0
  57. package/dist/capabilities/session/read-guard.js +75 -24
  58. package/dist/capabilities/session/static-flight-golden.js +150 -0
  59. package/dist/capabilities/session/transport.js +97 -83
  60. package/dist/capabilities/session/wizard.js +117 -0
  61. package/dist/capabilities/session-consent.js +178 -0
  62. package/dist/capabilities/session-login.js +170 -0
  63. package/dist/capabilities/session-search.js +635 -44
  64. package/dist/capabilities/spawn-bounded.js +203 -0
  65. package/dist/capabilities/sponsor-plugin.js +341 -0
  66. package/dist/capabilities/visa-policy.js +140 -0
  67. package/dist/capabilities/weather.js +243 -138
  68. package/dist/data/session-golden-20.json +25 -0
  69. package/dist/data/sf-golden-manifest.json +103 -0
  70. package/dist/data/sf-static-routes.json +91 -0
  71. package/dist/scripts/action-cache-tests.js +24 -59
  72. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  73. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  74. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  75. package/dist/scripts/agent-reach-error-tests.js +354 -0
  76. package/dist/scripts/agent-reach-tests.js +26 -18
  77. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  78. package/dist/scripts/anything-path-measure-tests.js +133 -0
  79. package/dist/scripts/anything-path-measure.js +150 -0
  80. package/dist/scripts/anything-tests.js +250 -60
  81. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  82. package/dist/scripts/artifacts-capability-tests.js +727 -0
  83. package/dist/scripts/async-collect.js +50 -11
  84. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  85. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  86. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  87. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  88. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  89. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  90. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  91. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  92. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  93. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  94. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  95. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +126 -0
  96. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  97. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  98. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  99. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  100. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  101. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  102. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  103. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  104. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  105. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  106. package/dist/scripts/booking-executor-tests.js +80 -0
  107. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  108. package/dist/scripts/booking-saga-tests.js +125 -0
  109. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  110. package/dist/scripts/bootstrap-tests.js +1549 -0
  111. package/dist/scripts/bridge-events-tests.js +239 -0
  112. package/dist/scripts/bridge-ledger-tests.js +428 -0
  113. package/dist/scripts/build-changelog.js +238 -0
  114. package/dist/scripts/build-metrics-report.js +331 -0
  115. package/dist/scripts/changelog-tests.js +118 -0
  116. package/dist/scripts/channel-probe-tests.js +103 -0
  117. package/dist/scripts/channel-probe.js +169 -0
  118. package/dist/scripts/channel-registry-tests.js +208 -0
  119. package/dist/scripts/city-scenario-tier-tests.js +234 -0
  120. package/dist/scripts/companion-tests.js +25 -70
  121. package/dist/scripts/dida-cookie-probe.js +87 -0
  122. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  123. package/dist/scripts/dida-sw-probe.js +114 -0
  124. package/dist/scripts/diff-test.js +17 -12
  125. package/dist/scripts/doctor-tests.js +600 -0
  126. package/dist/scripts/drill-multiuser-ledger-tests.js +1076 -0
  127. package/dist/scripts/drill-sdk-descendant-cleanup-tests.js +606 -0
  128. package/dist/scripts/drill-w2a-sensor-tests.js +788 -0
  129. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  130. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  131. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  132. package/dist/scripts/dsh-llm-tests.js +437 -0
  133. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  134. package/dist/scripts/dsh-runtime-closure.js +162 -0
  135. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  136. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  137. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  138. package/dist/scripts/effect-tests.js +539 -0
  139. package/dist/scripts/engine-run.js +1 -3
  140. package/dist/scripts/engine-tests.js +16 -10
  141. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  142. package/dist/scripts/evaluation-contract-tests.js +268 -0
  143. package/dist/scripts/extension-distribution-cli.js +39 -0
  144. package/dist/scripts/extension-distribution-tests.js +443 -0
  145. package/dist/scripts/extension-tests.js +988 -0
  146. package/dist/scripts/external-event-tests.js +350 -0
  147. package/dist/scripts/fact-gate-tests.js +869 -0
  148. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  149. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  150. package/dist/scripts/flight-malformed-tests.js +159 -0
  151. package/dist/scripts/flyai-setup-tests.js +228 -0
  152. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  153. package/dist/scripts/flyai-tests.js +692 -0
  154. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  155. package/dist/scripts/fx-contract-tests.js +201 -0
  156. package/dist/scripts/g5-guard-tests.js +150 -0
  157. package/dist/scripts/g5-guard.js +193 -0
  158. package/dist/scripts/gen-lunar-anchors.js +106 -0
  159. package/dist/scripts/geo-atlas-tests.js +283 -0
  160. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  161. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  162. package/dist/scripts/gotry-try-demo.js +49 -0
  163. package/dist/scripts/ground-transfer-tests.js +956 -0
  164. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  165. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  166. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  167. package/dist/scripts/hbcli-tests.js +121 -69
  168. package/dist/scripts/health-watch-cli.js +73 -0
  169. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  170. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  171. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  172. package/dist/scripts/i18n-tests.js +41 -0
  173. package/dist/scripts/incident-tests.js +281 -87
  174. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  175. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  176. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  177. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  178. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  179. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  180. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  181. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  182. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  183. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  184. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  185. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  186. package/dist/scripts/itinerary-deck-tests.js +435 -0
  187. package/dist/scripts/itinerary-html-tests.js +426 -0
  188. package/dist/scripts/journey-tests.js +22 -27
  189. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  190. package/dist/scripts/kernel-manifest-gate.js +126 -0
  191. package/dist/scripts/kernel-manifest-tests.js +212 -0
  192. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  193. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  194. package/dist/scripts/kernel-manifest.js +369 -0
  195. package/dist/scripts/lavish-local-tests.js +1226 -0
  196. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  197. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  198. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  199. package/dist/scripts/ledger-tests.js +382 -246
  200. package/dist/scripts/ledger-workflow-crash.js +10 -20
  201. package/dist/scripts/m3-cohort-tests.js +714 -0
  202. package/dist/scripts/m3-cohort.js +310 -0
  203. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  204. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  205. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  206. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  207. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  208. package/dist/scripts/memory-capture-tests.js +242 -61
  209. package/dist/scripts/memory-decay-tests.js +18 -52
  210. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  211. package/dist/scripts/memory-lifecycle.js +196 -0
  212. package/dist/scripts/memory-metrics.js +24 -12
  213. package/dist/scripts/memory-value-report.js +631 -0
  214. package/dist/scripts/memory-value-tests.js +328 -0
  215. package/dist/scripts/metrics-report-tests.js +151 -0
  216. package/dist/scripts/model-override-e2e.js +137 -0
  217. package/dist/scripts/nightly-evidence-tests.js +203 -0
  218. package/dist/scripts/nightly-evidence.js +303 -0
  219. package/dist/scripts/nudge-digest.js +39 -28
  220. package/dist/scripts/observability-tests.js +381 -0
  221. package/dist/scripts/onboarding-tests.js +161 -0
  222. package/dist/scripts/opensky-check.js +18 -13
  223. package/dist/scripts/opensky-tests.js +28 -28
  224. package/dist/scripts/outcome-projection-tests.js +427 -0
  225. package/dist/scripts/persona-sim-tests.js +603 -0
  226. package/dist/scripts/persona-sim.js +1081 -0
  227. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  228. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  229. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  230. package/dist/scripts/price-drift-tests.js +340 -0
  231. package/dist/scripts/price-drift-watch.js +447 -0
  232. package/dist/scripts/probe-poi-tests.js +53 -45
  233. package/dist/scripts/product-metrics.js +483 -0
  234. package/dist/scripts/publish-preverify.js +110 -0
  235. package/dist/scripts/realtime-pricing-tests.js +76 -0
  236. package/dist/scripts/recall-tests.js +513 -0
  237. package/dist/scripts/replay-async.js +22 -18
  238. package/dist/scripts/replay-real.js +18 -18
  239. package/dist/scripts/replay.js +54 -82
  240. package/dist/scripts/route-provider-conformance-tests.js +391 -0
  241. package/dist/scripts/session-attach-diagnose.js +20 -20
  242. package/dist/scripts/session-attach-poc.js +34 -37
  243. package/dist/scripts/session-benchmark.js +180 -0
  244. package/dist/scripts/session-dida-live-e2e.js +199 -0
  245. package/dist/scripts/session-extract-tests.js +14 -35
  246. package/dist/scripts/session-link-tests.js +478 -0
  247. package/dist/scripts/session-login.js +27 -46
  248. package/dist/scripts/session-tests.js +796 -96
  249. package/dist/scripts/session-zone-crash.js +49 -0
  250. package/dist/scripts/session-zone-ledger-tests.js +432 -0
  251. package/dist/scripts/session-zone-observation-tests.js +492 -0
  252. package/dist/scripts/session-zone-observe.js +135 -0
  253. package/dist/scripts/session-zone-wiring-tests.js +566 -0
  254. package/dist/scripts/session-zones-tests.js +374 -0
  255. package/dist/scripts/sf-live-benchmark.js +367 -0
  256. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  257. package/dist/scripts/sf-live-cli-tests.js +92 -0
  258. package/dist/scripts/sf-live-options.js +46 -0
  259. package/dist/scripts/sf-manifest-tests.js +30 -0
  260. package/dist/scripts/sf-manifest.js +33 -0
  261. package/dist/scripts/sf-soft-score-tests.js +67 -0
  262. package/dist/scripts/sf-summary-tests.js +324 -0
  263. package/dist/scripts/sf-summary.js +525 -0
  264. package/dist/scripts/share-tests.js +432 -0
  265. package/dist/scripts/skeleton-check.js +16 -14
  266. package/dist/scripts/skeleton-integration-test.js +28 -11
  267. package/dist/scripts/skills-contract-tests.js +51 -46
  268. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  269. package/dist/scripts/smoke.js +690 -246
  270. package/dist/scripts/spawn-bounded-abort-tests.js +244 -0
  271. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  272. package/dist/scripts/state-cli-tests.js +226 -101
  273. package/dist/scripts/state-cli.js +565 -198
  274. package/dist/scripts/state-repair-cli.js +66 -0
  275. package/dist/scripts/state-repair-tests.js +140 -0
  276. package/dist/scripts/static-golden-tests.js +165 -0
  277. package/dist/scripts/time-eval-tests.js +212 -192
  278. package/dist/scripts/travel-timeline-tests.js +29 -93
  279. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  280. package/dist/scripts/turn-handoff-collect.js +161 -0
  281. package/dist/scripts/turn-policy-tests.js +55 -0
  282. package/dist/scripts/typed-contract-canary.js +176 -0
  283. package/dist/scripts/tz-resolver-sanity.js +50 -0
  284. package/dist/scripts/tz-resolver-tests.js +584 -0
  285. package/dist/scripts/unified-tests.js +60 -20
  286. package/dist/scripts/visa-policy-tests.js +62 -0
  287. package/dist/scripts/weather-tests.js +276 -41
  288. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  289. package/dist/scripts/write-gate-tests.js +522 -0
  290. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  291. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  292. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  293. package/dist/scripts/z3-race-tests.js +46 -0
  294. package/dist/src/artifact-gate.js +829 -0
  295. package/dist/src/backend/kernel.js +91 -0
  296. package/dist/src/backend/modules/booking-copilot.js +92 -0
  297. package/dist/src/backend/modules/booking-executor.js +124 -0
  298. package/dist/src/backend/modules/session-search.js +551 -0
  299. package/dist/src/benchmark-agent-conformance.js +578 -0
  300. package/dist/src/benchmark-environment-bridge.js +515 -0
  301. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  302. package/dist/src/benchmark-tool-isolation.js +151 -0
  303. package/dist/src/bookable-facts.js +533 -0
  304. package/dist/src/booking-saga.js +97 -0
  305. package/dist/src/booking-surface/availability-policy.js +697 -0
  306. package/dist/src/booking-surface/booking-intent.js +43 -0
  307. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  308. package/dist/src/booking-surface/canonical-schema.js +116 -0
  309. package/dist/src/booking-surface/contracts.js +75 -0
  310. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  311. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  312. package/dist/src/booking-surface/dsh-worker.js +98 -0
  313. package/dist/src/booking-surface/error-codes.js +231 -0
  314. package/dist/src/booking-surface/index.js +9 -0
  315. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  316. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  317. package/dist/src/booking-surface/profile.js +24 -0
  318. package/dist/src/booking-surface/recovery-chain.js +367 -0
  319. package/dist/src/booking-surface/runtime.js +1968 -0
  320. package/dist/src/booking-surface/server.js +674 -0
  321. package/dist/src/booking-surface/startup.js +168 -0
  322. package/dist/src/booking-surface/validation.js +261 -0
  323. package/dist/src/bridge.js +20 -13
  324. package/dist/src/city-scenario-tier.js +378 -0
  325. package/dist/src/companions.js +48 -74
  326. package/dist/src/contracts.js +21 -43
  327. package/dist/src/dsh-llm.js +171 -115
  328. package/dist/src/engine.js +276 -193
  329. package/dist/src/evaluation-cadence.js +97 -0
  330. package/dist/src/evaluation-contracts.js +283 -0
  331. package/dist/src/flight-pack-adapter.js +54 -0
  332. package/dist/src/flight-pack-contract.js +2 -0
  333. package/dist/src/flyai-setup-tool.js +70 -0
  334. package/dist/src/gotry-backend.js +125 -0
  335. package/dist/src/hotel-date-gate.js +118 -0
  336. package/dist/src/i18n.js +88 -0
  337. package/dist/src/index.js +2385 -881
  338. package/dist/src/itinerary-deck.js +139 -0
  339. package/dist/src/itinerary-doc-shared.js +661 -0
  340. package/dist/src/itinerary-html.js +131 -0
  341. package/dist/src/journey.js +155 -108
  342. package/dist/src/lavish-tools.js +400 -0
  343. package/dist/src/ledger-repair-apply.js +538 -0
  344. package/dist/src/ledger-repair-plan.js +463 -0
  345. package/dist/src/loop.js +331 -161
  346. package/dist/src/m3-cohort.js +1577 -0
  347. package/dist/src/memory-capture.js +180 -32
  348. package/dist/src/memory-decay.js +30 -14
  349. package/dist/src/memory-lifecycle.js +1310 -0
  350. package/dist/src/memory-utility.js +27 -40
  351. package/dist/src/mock-llm.js +36 -61
  352. package/dist/src/model.js +81 -36
  353. package/dist/src/outcome-projection.js +746 -0
  354. package/dist/src/realtime-pricing.js +103 -0
  355. package/dist/src/recall/card.js +74 -0
  356. package/dist/src/recall/evaluator.js +140 -0
  357. package/dist/src/recall/integration.js +17 -0
  358. package/dist/src/recall/tick.js +166 -0
  359. package/dist/src/session-link/plan-it-action.js +48 -0
  360. package/dist/src/session-link/session-link.js +248 -0
  361. package/dist/src/session-zone-ledger.js +312 -0
  362. package/dist/src/session-zone-observation.js +354 -0
  363. package/dist/src/session-zone-wiring.js +658 -0
  364. package/dist/src/session-zones.js +510 -0
  365. package/dist/src/share/adapters.js +51 -0
  366. package/dist/src/share/share-consent.js +76 -0
  367. package/dist/src/share/share-deck.js +58 -0
  368. package/dist/src/share/share-token.js +146 -0
  369. package/dist/src/slot-spec.js +84 -106
  370. package/dist/src/state-ledger.js +696 -524
  371. package/dist/src/state-repair.js +299 -0
  372. package/dist/src/subagent-job-id-guard.js +58 -0
  373. package/dist/src/time-anchor.js +231 -33
  374. package/dist/src/tool-packet.js +20 -8
  375. package/dist/src/travel-slots.js +58 -35
  376. package/dist/src/travel-timeline.js +44 -63
  377. package/dist/src/turn-deadline.js +287 -0
  378. package/dist/src/turn-policy.js +118 -0
  379. package/dist/src/tz-resolver.js +143 -0
  380. package/dist/src/unified.js +600 -360
  381. package/dist/src/wish-pool.js +42 -12
  382. package/dist/src/write-gate.js +495 -0
  383. package/dist/src/z3-shared.js +284 -0
  384. package/extension/README.md +21 -0
  385. package/extension/README.zh-CN.md +21 -0
  386. package/extension/background.js +463 -0
  387. package/extension/content-bridge.js +270 -0
  388. package/extension/content-main.js +111 -0
  389. package/extension/manifest.json +60 -0
  390. package/package.json +407 -14
  391. package/schemas/booking.intent.schema.json +193 -0
  392. package/schemas/booking.surface.schema.json +2622 -0
  393. package/ts/capabilities/agent-reach-deep.ts +114 -0
  394. package/ts/capabilities/agent-reach.ts +198 -0
  395. package/ts/capabilities/anything.ts +282 -0
  396. package/ts/capabilities/artifacts.ts +428 -0
  397. package/ts/capabilities/fact-log.ts +54 -0
  398. package/ts/capabilities/flyai-config.ts +388 -0
  399. package/ts/capabilities/flyai.ts +1274 -0
  400. package/ts/capabilities/hbcli.ts +253 -71
  401. package/ts/capabilities/incident-log.ts +88 -59
  402. package/ts/capabilities/itinerary-artifact.ts +214 -0
  403. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  404. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  405. package/ts/capabilities/opensky.ts +121 -0
  406. package/ts/capabilities/session/action-cache.ts +148 -0
  407. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  408. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  409. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  410. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  411. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  412. package/ts/capabilities/session/benchmark.ts +276 -0
  413. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  414. package/ts/capabilities/session/extension-bridge.ts +794 -0
  415. package/ts/capabilities/session/extension-channel.ts +144 -0
  416. package/ts/capabilities/session/extension-distribution.ts +266 -0
  417. package/ts/capabilities/session/extract.ts +54 -0
  418. package/ts/capabilities/session/golden-score.ts +139 -0
  419. package/ts/capabilities/session/health-watch.ts +264 -0
  420. package/ts/capabilities/session/read-guard.ts +141 -0
  421. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  422. package/ts/capabilities/session/transport.ts +146 -0
  423. package/ts/capabilities/session/wizard.ts +140 -0
  424. package/ts/capabilities/session-consent.ts +211 -0
  425. package/ts/capabilities/session-login.ts +208 -0
  426. package/ts/capabilities/session-search.ts +893 -0
  427. package/ts/capabilities/weather.ts +330 -0
  428. package/ts/dsh-runtime/vendor/README.md +68 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  440. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  441. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  442. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  443. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  444. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  445. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  446. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  447. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  448. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  449. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  450. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  451. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  452. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  453. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  454. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  455. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  456. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  457. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  458. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  459. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  460. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  461. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  462. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  463. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  464. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  465. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  466. package/ts/package.json +293 -3
  467. package/ts/scripts/async-collect.ts +91 -0
  468. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  469. package/ts/scripts/skeleton-check.ts +1 -1
  470. package/ts/scripts/skeleton-integration-test.ts +23 -1
  471. package/ts/scripts/state-cli.ts +586 -0
  472. package/ts/scripts/turn-handoff-collect.ts +178 -0
  473. package/ts/src/artifact-gate.ts +991 -0
  474. package/ts/src/benchmark-agent-conformance.ts +619 -0
  475. package/ts/src/benchmark-environment-bridge.ts +538 -0
  476. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  477. package/ts/src/benchmark-tool-isolation.ts +166 -0
  478. package/ts/src/bookable-facts.ts +837 -0
  479. package/ts/src/booking-saga.ts +133 -0
  480. package/ts/src/booking-surface/availability-policy.ts +522 -0
  481. package/ts/src/booking-surface/booking-intent.ts +86 -0
  482. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  483. package/ts/src/booking-surface/canonical-schema.js +116 -0
  484. package/ts/src/booking-surface/contracts.ts +475 -0
  485. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  486. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  487. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  488. package/ts/src/booking-surface/error-codes.ts +241 -0
  489. package/ts/src/booking-surface/index.ts +9 -0
  490. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  491. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  492. package/ts/src/booking-surface/profile.ts +42 -0
  493. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  494. package/ts/src/booking-surface/runtime.ts +1793 -0
  495. package/ts/src/booking-surface/server.ts +733 -0
  496. package/ts/src/booking-surface/startup.ts +201 -0
  497. package/ts/src/booking-surface/validation.ts +217 -0
  498. package/ts/src/companions.ts +113 -0
  499. package/ts/src/contracts.ts +17 -3
  500. package/ts/src/dsh-llm.ts +152 -17
  501. package/ts/src/flyai-setup-tool.ts +77 -0
  502. package/ts/src/index.ts +2123 -200
  503. package/ts/src/itinerary-deck.ts +189 -0
  504. package/ts/src/itinerary-doc-shared.ts +741 -0
  505. package/ts/src/itinerary-html.ts +171 -0
  506. package/ts/src/loop.ts +234 -26
  507. package/ts/src/memory-capture.ts +283 -0
  508. package/ts/src/memory-decay.ts +55 -0
  509. package/ts/src/memory-utility.ts +70 -0
  510. package/ts/src/mock-llm.ts +4 -8
  511. package/ts/src/model.ts +88 -2
  512. package/ts/src/recall/card.ts +106 -0
  513. package/ts/src/recall/evaluator.ts +170 -0
  514. package/ts/src/recall/integration.ts +35 -0
  515. package/ts/src/recall/tick.ts +213 -0
  516. package/ts/src/session-link/plan-it-action.ts +78 -0
  517. package/ts/src/session-link/session-link.ts +275 -0
  518. package/ts/src/share/adapters.ts +93 -0
  519. package/ts/src/share/share-consent.ts +105 -0
  520. package/ts/src/share/share-deck.ts +85 -0
  521. package/ts/src/share/share-token.ts +166 -0
  522. package/ts/src/slot-spec.ts +172 -0
  523. package/ts/src/state-ledger.ts +1166 -0
  524. package/ts/src/subagent-job-id-guard.ts +72 -0
  525. package/ts/src/time-anchor.ts +338 -0
  526. package/ts/src/tool-packet.ts +32 -0
  527. package/ts/src/travel-slots.ts +219 -0
  528. package/ts/src/travel-timeline.ts +83 -0
  529. package/ts/src/turn-deadline.ts +366 -0
  530. package/ts/src/turn-policy.ts +154 -0
  531. package/ts/src/unified.ts +444 -105
  532. package/ts/src/wish-pool.ts +79 -0
  533. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,733 @@
1
+ /**
2
+ * BFF-only HTTP/SSE seam for the embedded Booking Copilot.
3
+ *
4
+ * The browser never calls this server directly. A same-origin HotelByte BFF
5
+ * authenticates the actor, mints contextRef and sends a strict planner turn
6
+ * with a deployment-level Bearer key. No portal token field exists here.
7
+ */
8
+
9
+ import { timingSafeEqual } from 'node:crypto'
10
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'
11
+ import {
12
+ BOOKING_COPILOT_ACCEPTED_TURN_KINDS,
13
+ BOOKING_COPILOT_INGRESS_MODES,
14
+ BOOKING_READ_ACTION_KINDS,
15
+ BOOKING_SURFACE_ALLOWED_ACTIONS,
16
+ BOOKING_SURFACE_FEATURES,
17
+ BOOKING_SURFACE_FEATURES_HEADER,
18
+ BOOKING_SURFACE_SCHEMA_SHA256,
19
+ BOOKING_SURFACE_SCHEMA_SHA256_HEADER,
20
+ BOOKING_SURFACE_SCHEMA_VERSION,
21
+ BOOKING_SURFACE_VERSION_HEADER,
22
+ bookingSurfaceActionsForFeatures,
23
+ type BookingCopilotIngressMode,
24
+ type BookingCopilotTurn,
25
+ type BookingIngressBinding,
26
+ type BookingIngressPrincipal,
27
+ type BookingReadActionKind,
28
+ type BookingSurfaceFeature,
29
+ type BookingSurfaceEvent,
30
+ type BookingWorkspaceSnapshot,
31
+ type ObservableOrderFact,
32
+ type IngressTurn,
33
+ } from './contracts.ts'
34
+ import {
35
+ bookingDigest,
36
+ BookingCopilotTaskRuntime,
37
+ type BookingCopilotTaskState,
38
+ type BookingIngressRequestBindingInput,
39
+ type BookingPlannerSessionFactory,
40
+ } from './runtime.ts'
41
+ import { validateBookingSurface } from './validation.ts'
42
+ import { extractBookingDispatchReason, normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
43
+
44
+ export interface BookingCopilotComposition {
45
+ runtime: BookingCopilotTaskRuntime
46
+ plannerFactory: BookingPlannerSessionFactory
47
+ /** Optional BFF-owned identity binding. GoTry never accepts browser identity. */
48
+ ingressBinding?: BookingIngressBinding
49
+ /** Principal authenticated by the HotelByte BFF, never by browser input. */
50
+ principal?: BookingIngressPrincipal
51
+ /** Explicit process composition mode; omitted means infer from a complete pair. */
52
+ ingressMode?: BookingCopilotIngressMode
53
+ }
54
+
55
+ export interface BookingCopilotServerOptions extends BookingCopilotComposition {
56
+ /** Deployment credential shared only with the HotelByte BFF. */
57
+ apiKey: string
58
+ host?: string
59
+ port?: number
60
+ maxBodyBytes?: number
61
+ /** GoTry source commit loaded from the root-owned release artifact. */
62
+ artifactId?: string
63
+ }
64
+
65
+ export interface BookingCopilotServerHandle {
66
+ server: Server
67
+ port: number
68
+ close(): Promise<void>
69
+ }
70
+
71
+ export interface BookingCopilotRuntimeIdentity {
72
+ nodeVersion: string
73
+ nodeModulesAbi: string
74
+ releaseTuple: string
75
+ glibcVersion: string
76
+ }
77
+
78
+ /** gotry-backend 模块挂载用的流量处理依赖(startBookingCopilotServer 与 backend 模块共用) */
79
+ export interface BookingCopilotTrafficDeps {
80
+ apiKey: string
81
+ composition: BookingCopilotComposition
82
+ maxBodyBytes: number
83
+ artifactId?: string
84
+ ingressMode: BookingCopilotIngressMode
85
+ runningIdentity: BookingCopilotRuntimeIdentity
86
+ }
87
+
88
+ /**
89
+ * 单请求处理闭包(startBookingCopilotServer 的 createServer 回调原样摘出):
90
+ * 探活(/healthz /status)+ 唯一 turn 路由 + 鉴权 + schema 头校验。
91
+ * gotry-backend 的 booking-copilot 模块复用同一闭包,保证两条挂载路径行为逐字一致。
92
+ */
93
+ export function bookingCopilotTrafficHandler(deps: BookingCopilotTrafficDeps): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
94
+ const { apiKey, composition, maxBodyBytes, artifactId, ingressMode, runningIdentity } = deps
95
+ return async (req, res) => {
96
+ const isProbe = req.method === 'GET' && (req.url === '/healthz' || req.url === '/status')
97
+ if (!isProbe && (req.method !== 'POST' || req.url !== '/a2a/booking-copilot/turn')) {
98
+ sendJson(res, 404, { error: { code: 'not_found' } })
99
+ return
100
+ }
101
+ const auth = String(req.headers.authorization ?? '')
102
+ if (!safeSecretEqual(auth, `Bearer ${apiKey}`)) {
103
+ sendJson(res, 401, { error: { code: 'unauthorized' } })
104
+ return
105
+ }
106
+ if (lifecycleFor(composition).shuttingDown) {
107
+ sendJson(res, 503, { error: { code: 'booking_copilot_shutting_down' } })
108
+ return
109
+ }
110
+ if (isProbe) {
111
+ res.setHeader(BOOKING_SURFACE_VERSION_HEADER, BOOKING_SURFACE_SCHEMA_VERSION)
112
+ res.setHeader(BOOKING_SURFACE_SCHEMA_SHA256_HEADER, BOOKING_SURFACE_SCHEMA_SHA256)
113
+ res.setHeader(BOOKING_SURFACE_FEATURES_HEADER, BOOKING_SURFACE_FEATURES.join(','))
114
+ if (artifactId) res.setHeader('X-GoTry-Artifact-ID', artifactId)
115
+ res.setHeader('X-GoTry-Node-Version', runningIdentity.nodeVersion)
116
+ res.setHeader('X-GoTry-Node-Modules-ABI', runningIdentity.nodeModulesAbi)
117
+ res.setHeader('X-GoTry-Release-Tuple', runningIdentity.releaseTuple)
118
+ if (runningIdentity.glibcVersion) res.setHeader('X-GoTry-Glibc-Version', runningIdentity.glibcVersion)
119
+ const acceptedTurnKinds = ingressMode === 'bff-ingress-binding'
120
+ ? [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS, 'user.turn.ingress' as const]
121
+ : [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS]
122
+ const healthBody: Record<string, unknown> = {
123
+ schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
124
+ schemaSha256: BOOKING_SURFACE_SCHEMA_SHA256,
125
+ status: 'ready',
126
+ ingressMode,
127
+ acceptedTurnKinds,
128
+ features: [...BOOKING_SURFACE_FEATURES],
129
+ }
130
+ res.setHeader('X-GoTry-Ingress-Mode', ingressMode)
131
+ res.setHeader('X-GoTry-Accepted-Turn-Kinds', acceptedTurnKinds.join(','))
132
+ sendJson(res, 200, healthBody)
133
+ return
134
+ }
135
+ const schemaVersion = String(req.headers[BOOKING_SURFACE_VERSION_HEADER] ?? '')
136
+ const schemaHash = String(req.headers[BOOKING_SURFACE_SCHEMA_SHA256_HEADER] ?? '')
137
+ // `booking.surface` is the v1 compatibility line. Compatible additive
138
+ // releases keep this version and may have different schema bytes; the
139
+ // canonical payload validator below remains authoritative. Only a breaking
140
+ // contract line changes this version and is rejected at the version gate.
141
+ if (schemaVersion !== BOOKING_SURFACE_SCHEMA_VERSION) {
142
+ sendJson(res, 409, {
143
+ error: {
144
+ code: 'booking_surface_schema_mismatch',
145
+ expectedVersion: BOOKING_SURFACE_SCHEMA_VERSION,
146
+ },
147
+ })
148
+ return
149
+ }
150
+ if (schemaHash && !/^[0-9a-f]{64}$/i.test(schemaHash)) {
151
+ sendJson(res, 400, { error: { code: 'booking_surface_schema_hash_invalid' } })
152
+ return
153
+ }
154
+ const lifecycle = lifecycleFor(composition)
155
+ lifecycle.acceptedRequests += 1
156
+ try {
157
+ await handleBookingCopilotRequest(req, res, composition, maxBodyBytes, negotiatedBookingSurfaceFeatures(req))
158
+ } finally {
159
+ finishAcceptedRequest(lifecycle)
160
+ }
161
+ }
162
+ }
163
+
164
+ type PlannerSession = ReturnType<BookingPlannerSessionFactory>
165
+
166
+ interface PlannerLifecycle {
167
+ active: Map<string, PlannerSession>
168
+ /** Close operations detached from request latency, but still owned by the composition. */
169
+ closing: Set<Promise<void>>
170
+ /** Sticky cleanup failures surfaced by shutdown; close is not assumed retryable. */
171
+ cleanupFailures: Map<PlannerSession, unknown>
172
+ /** Turn handlers admitted before shutdown began. */
173
+ acceptedRequests: number
174
+ acceptedRequestDrainWaiters: Set<() => void>
175
+ shuttingDown: boolean
176
+ shutdownPromise?: Promise<void>
177
+ }
178
+
179
+ const plannerLifecycles = new WeakMap<BookingCopilotComposition, PlannerLifecycle>()
180
+ const decisionFlightsByComposition = new WeakMap<BookingCopilotComposition, Map<string, Promise<BookingSurfaceEvent[]>>>()
181
+
182
+ function lifecycleFor(composition: BookingCopilotComposition): PlannerLifecycle {
183
+ let lifecycle = plannerLifecycles.get(composition)
184
+ if (!lifecycle) {
185
+ lifecycle = {
186
+ active: new Map(),
187
+ closing: new Set(),
188
+ cleanupFailures: new Map(),
189
+ acceptedRequests: 0,
190
+ acceptedRequestDrainWaiters: new Set(),
191
+ shuttingDown: false,
192
+ }
193
+ plannerLifecycles.set(composition, lifecycle)
194
+ }
195
+ return lifecycle
196
+ }
197
+
198
+ function finishAcceptedRequest(lifecycle: PlannerLifecycle): void {
199
+ lifecycle.acceptedRequests -= 1
200
+ if (lifecycle.acceptedRequests !== 0) return
201
+ for (const resolve of lifecycle.acceptedRequestDrainWaiters) resolve()
202
+ lifecycle.acceptedRequestDrainWaiters.clear()
203
+ }
204
+
205
+ function drainAcceptedRequests(composition: BookingCopilotComposition): Promise<void> {
206
+ const lifecycle = lifecycleFor(composition)
207
+ if (lifecycle.acceptedRequests === 0) return Promise.resolve()
208
+ return new Promise((resolve) => lifecycle.acceptedRequestDrainWaiters.add(resolve))
209
+ }
210
+
211
+ function sessionsFor(composition: BookingCopilotComposition): Map<string, PlannerSession> {
212
+ return lifecycleFor(composition).active
213
+ }
214
+
215
+ function schedulePlannerSessionClose(composition: BookingCopilotComposition, session: PlannerSession): void {
216
+ const lifecycle = lifecycleFor(composition)
217
+ const cleanup = Promise.resolve().then(() => session.close?.()).then(
218
+ () => { lifecycle.cleanupFailures.delete(session) },
219
+ (error: unknown) => {
220
+ lifecycle.cleanupFailures.set(session, error)
221
+ // Cleanup must not replace an already-durable booking outcome. Keep the
222
+ // failure machine-readable and free of task/prompt/PII values. Session
223
+ // close promises are allowed to be sticky, so shutdown reports the
224
+ // original failure rather than retrying and laundering it into success.
225
+ console.error(JSON.stringify({ code: 'PLANNER_SESSION_CLOSE_FAILED' }))
226
+ },
227
+ )
228
+ lifecycle.closing.add(cleanup)
229
+ void cleanup.then(() => lifecycle.closing.delete(cleanup))
230
+ }
231
+
232
+ /** Detach a session from the request path and close it in the composition lifecycle. */
233
+ function releasePlannerSession(composition: BookingCopilotComposition, taskId: string): void {
234
+ const sessions = sessionsFor(composition)
235
+ const session = sessions.get(taskId)
236
+ if (!session) return
237
+ sessions.delete(taskId)
238
+ schedulePlannerSessionClose(composition, session)
239
+ }
240
+
241
+ async function drainPlannerSessions(composition: BookingCopilotComposition): Promise<void> {
242
+ const lifecycle = lifecycleFor(composition)
243
+ for (const [taskId, session] of lifecycle.active) {
244
+ lifecycle.active.delete(taskId)
245
+ schedulePlannerSessionClose(composition, session)
246
+ }
247
+ while (lifecycle.closing.size > 0) {
248
+ await Promise.all([...lifecycle.closing])
249
+ }
250
+
251
+ if (lifecycle.cleanupFailures.size > 0) {
252
+ throw new AggregateError([...lifecycle.cleanupFailures.values()], 'planner session cleanup failed during shutdown')
253
+ }
254
+ }
255
+
256
+ async function drainDecisionFlights(composition: BookingCopilotComposition): Promise<void> {
257
+ const flights = decisionFlightsFor(composition)
258
+ while (flights.size > 0) {
259
+ await Promise.allSettled([...flights.values()])
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Stop admitting turns and drain every planner resource owned by a composition.
265
+ * This is shared by the standalone listener and the unified gotry-backend
266
+ * module, whose listener is owned by the backend kernel.
267
+ */
268
+ export function shutdownBookingCopilotTraffic(composition: BookingCopilotComposition): Promise<void> {
269
+ const lifecycle = lifecycleFor(composition)
270
+ if (lifecycle.shutdownPromise) return lifecycle.shutdownPromise
271
+ lifecycle.shuttingDown = true
272
+ lifecycle.shutdownPromise = (async () => {
273
+ await drainAcceptedRequests(composition)
274
+ await drainDecisionFlights(composition)
275
+ await drainPlannerSessions(composition)
276
+ })()
277
+ return lifecycle.shutdownPromise
278
+ }
279
+ function decisionFlightsFor(composition: BookingCopilotComposition): Map<string, Promise<BookingSurfaceEvent[]>> {
280
+ let flights = decisionFlightsByComposition.get(composition)
281
+ if (!flights) { flights = new Map(); decisionFlightsByComposition.set(composition, flights) }
282
+ return flights
283
+ }
284
+ async function runDecisionSingleFlight(composition: BookingCopilotComposition, key: string, work: () => Promise<BookingSurfaceEvent[]>): Promise<BookingSurfaceEvent[]> {
285
+ const flights = decisionFlightsFor(composition)
286
+ const prior = flights.get(key)
287
+ if (prior) return prior
288
+ const current = work()
289
+ flights.set(key, current)
290
+ try { return await current } finally { if (flights.get(key) === current) flights.delete(key) }
291
+ }
292
+
293
+ export function runtimeIdentity(): BookingCopilotRuntimeIdentity {
294
+ const report = process.report?.getReport?.() as { header?: { glibcVersionRuntime?: string } } | undefined
295
+ const header = report?.header
296
+ const glibcVersion = header?.glibcVersionRuntime ?? ''
297
+ return {
298
+ nodeVersion: process.version,
299
+ nodeModulesAbi: process.versions.modules ?? '',
300
+ releaseTuple: `${process.platform}-${process.arch}-${glibcVersion ? 'glibc' : 'unknown'}`,
301
+ glibcVersion,
302
+ }
303
+ }
304
+
305
+ function safeSecretEqual(actual: string, expected: string): boolean {
306
+ const actualBytes = Buffer.from(actual)
307
+ const expectedBytes = Buffer.from(expected)
308
+ return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes)
309
+ }
310
+
311
+ function sendJson(res: ServerResponse, status: number, body: unknown): void {
312
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
313
+ res.end(JSON.stringify(body))
314
+ }
315
+
316
+ function readBody(req: IncomingMessage, maxBodyBytes: number): Promise<string> {
317
+ return new Promise((resolve, reject) => {
318
+ const chunks: Buffer[] = []
319
+ let size = 0
320
+ let exceeded = false
321
+ req.on('data', (chunk: Buffer) => {
322
+ size += chunk.length
323
+ if (size > maxBodyBytes) {
324
+ exceeded = true
325
+ return
326
+ }
327
+ chunks.push(chunk)
328
+ })
329
+ req.on('end', () => exceeded ? reject(new Error('payload_too_large')) : resolve(Buffer.concat(chunks).toString('utf8')))
330
+ req.on('error', reject)
331
+ })
332
+ }
333
+
334
+ function write(res: ServerResponse, event: BookingSurfaceEvent): void {
335
+ res.write(`event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`)
336
+ res.flushHeaders()
337
+ res.socket?.setNoDelay(true)
338
+ res.socket?.uncork()
339
+ }
340
+
341
+ function flush(res: ServerResponse): void {
342
+ res.flushHeaders()
343
+ const maybeFlush = res as ServerResponse & { flush?: () => void }
344
+ maybeFlush.flush?.()
345
+ }
346
+
347
+ function sameCapabilities(a: readonly string[], b: readonly string[]): boolean {
348
+ return a.length === b.length && [...a].sort().every((kind, index) => kind === [...b].sort()[index])
349
+ }
350
+ function exactKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
351
+ return Object.keys(value).length === allowed.length && Object.keys(value).every((key) => allowed.includes(key))
352
+ }
353
+
354
+ const SURFACE_ACTION_MATRIX = BOOKING_SURFACE_ALLOWED_ACTIONS
355
+
356
+ /**
357
+ * The authenticated BFF sends the fresh fleet-wide feature intersection in
358
+ * the same header that GoTry advertises on probes. Missing, malformed, and
359
+ * unknown values fail closed to the empty optional-feature set.
360
+ */
361
+ function negotiatedBookingSurfaceFeatures(req: IncomingMessage): BookingSurfaceFeature[] {
362
+ const raw = req.headers[BOOKING_SURFACE_FEATURES_HEADER]
363
+ if (typeof raw !== 'string' || raw.trim() === '') return []
364
+ const values = new Set(raw.split(',').map((value) => value.trim()).filter(Boolean))
365
+ return BOOKING_SURFACE_FEATURES.filter((feature) => values.has(feature))
366
+ }
367
+
368
+ function validSurfaceActions(surface: unknown, allowed: unknown): allowed is BookingWorkspaceSnapshot['capabilities']['allowedActions'] {
369
+ if (typeof surface !== 'string' || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, surface)) return false
370
+ if (!Array.isArray(allowed) || allowed.length < 1 || new Set(allowed).size !== allowed.length) return false
371
+ const matrix = SURFACE_ACTION_MATRIX[surface as BookingWorkspaceSnapshot['surface']]
372
+ return allowed.every((action) => typeof action === 'string'
373
+ && matrix.includes(action as BookingReadActionKind)
374
+ && (BOOKING_READ_ACTION_KINDS as readonly string[]).includes(action))
375
+ }
376
+
377
+ function validObservableOrders(value: unknown): value is ObservableOrderFact[] {
378
+ if (!Array.isArray(value) || value.length > 100) return false
379
+ const orderRefs = new Set<string>()
380
+ return value.every((item) => {
381
+ if (!item || typeof item !== 'object' || Array.isArray(item)) return false
382
+ const candidate = item as Record<string, unknown>
383
+ if (!exactKeys(candidate, ['orderRef', 'factRefs']) || typeof candidate.orderRef !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(candidate.orderRef) || orderRefs.has(candidate.orderRef)) return false
384
+ if (!Array.isArray(candidate.factRefs) || candidate.factRefs.length > 64 || new Set(candidate.factRefs).size !== candidate.factRefs.length || candidate.factRefs.some((ref) => typeof ref !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(ref))) return false
385
+ orderRefs.add(candidate.orderRef)
386
+ return true
387
+ })
388
+ }
389
+
390
+ function validBoundWorkspaceAuthority(workspace: BookingWorkspaceSnapshot): boolean {
391
+ return workspace.capabilities.surface === workspace.surface
392
+ && validSurfaceActions(workspace.surface, workspace.capabilities.allowedActions)
393
+ && (workspace.observableOrders === undefined || validObservableOrders(workspace.observableOrders))
394
+ }
395
+
396
+ /**
397
+ * Old compatible BFF releases may still send the base action matrix. Fresh
398
+ * tasks are narrowed to the negotiated optional features instead of failing
399
+ * the whole turn. Existing task allowlists remain stable across a rolling
400
+ * deploy, while unsupported trusted projections are still removed so no new
401
+ * optional action can gain authority.
402
+ */
403
+ function narrowWorkspaceOptionalAuthority(
404
+ workspace: BookingWorkspaceSnapshot,
405
+ features: readonly BookingSurfaceFeature[],
406
+ existingTaskActions?: readonly BookingReadActionKind[],
407
+ ): BookingWorkspaceSnapshot | null {
408
+ const negotiated = new Set(bookingSurfaceActionsForFeatures(workspace.surface, features))
409
+ if (existingTaskActions?.some((action) => !workspace.capabilities.allowedActions.includes(action))) return null
410
+ const allowedActions = existingTaskActions
411
+ ? [...existingTaskActions]
412
+ : workspace.capabilities.allowedActions.filter((action) => negotiated.has(action))
413
+ if (allowedActions.length === 0) return null
414
+ const { observableOrders, ...base } = workspace
415
+ return {
416
+ ...base,
417
+ capabilities: { surface: workspace.surface, allowedActions },
418
+ ...(features.includes('trusted-order-observation-v1') && observableOrders !== undefined
419
+ ? { observableOrders: observableOrders.map((order) => ({ orderRef: order.orderRef, factRefs: [...order.factRefs] })) }
420
+ : {}),
421
+ }
422
+ }
423
+
424
+ function validIngressBinding(binding: unknown, ingress: IngressTurn): binding is {
425
+ taskId: string
426
+ turnId: string
427
+ contextRef: string
428
+ surface: BookingWorkspaceSnapshot['surface']
429
+ allowedActions: BookingWorkspaceSnapshot['capabilities']['allowedActions']
430
+ observableOrders?: ObservableOrderFact[]
431
+ } {
432
+ if (!binding || typeof binding !== 'object' || Array.isArray(binding)) return false
433
+ const candidate = binding as Record<string, unknown>
434
+ if (!exactKeys(candidate, ['taskId', 'turnId', 'contextRef', 'surface', 'allowedActions'])
435
+ && !exactKeys(candidate, ['taskId', 'turnId', 'contextRef', 'surface', 'allowedActions', 'observableOrders'])) return false
436
+ if (typeof candidate.taskId !== 'string' || typeof candidate.turnId !== 'string' || typeof candidate.contextRef !== 'string') return false
437
+ if (typeof candidate.surface !== 'string' || candidate.surface !== ingress.surfaceHint || !Object.prototype.hasOwnProperty.call(SURFACE_ACTION_MATRIX, candidate.surface)) return false
438
+ return validSurfaceActions(candidate.surface, candidate.allowedActions)
439
+ && (candidate.observableOrders === undefined || validObservableOrders(candidate.observableOrders))
440
+ }
441
+
442
+ function validIngressApprovalAuthority(ingress: IngressTurn, binding: { taskId: string; contextRef: string }): boolean {
443
+ if (!ingress.request.approval) return true
444
+ return ingress.request.approval.taskId === binding.taskId
445
+ && ingress.request.approval.contextRef === binding.contextRef
446
+ }
447
+
448
+ function fallbackErrorEvent(task: BookingCopilotTaskState, code: string): BookingSurfaceEvent {
449
+ return {
450
+ schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION,
451
+ eventId: `error-${task.taskId}-${task.lastSequence + 1}`,
452
+ taskId: task.taskId,
453
+ contextRef: task.contextRef,
454
+ sequence: task.lastSequence + 1,
455
+ emittedAt: new Date().toISOString(),
456
+ kind: 'error',
457
+ error: { code: normalizeBookingErrorCode(code), message: safeBookingErrorMessage(normalizeBookingErrorCode(code)), retryable: false },
458
+ }
459
+ }
460
+
461
+ function writeTypedError(res: ServerResponse, composition: BookingCopilotComposition, task: BookingCopilotTaskState, error: unknown, requestKey?: string, includeSubmitted = false): void {
462
+ const code = normalizeBookingErrorCode(error)
463
+ // Provider/adapter exceptions can contain response bodies, URLs or request
464
+ // context. Preserve only the closed code and coarse thrown-value class.
465
+ console.error('[booking-copilot] planner boundary error:', JSON.stringify({
466
+ code,
467
+ thrownType: error instanceof Error ? 'error' : typeof error,
468
+ }))
469
+ try {
470
+ const decision = { kind: 'error' as const, error: { code, message: safeBookingErrorMessage(code), retryable: false } }
471
+ if (requestKey) {
472
+ const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, [decision], includeSubmitted, { suppressProgressStatuses: true })
473
+ for (const event of events) write(res, event)
474
+ return
475
+ }
476
+ write(res, composition.runtime.emitEvent(task.taskId, decision))
477
+ } catch {
478
+ // The stream is already committed. Emit a schema-valid, non-durable
479
+ // typed envelope instead of silently returning an empty HTTP 200 stream.
480
+ write(res, fallbackErrorEvent(task, code))
481
+ }
482
+ }
483
+
484
+ /** Handles one already-authenticated request inside the sole HTTP listener. */
485
+ async function handleBookingCopilotRequest(
486
+ req: IncomingMessage,
487
+ res: ServerResponse,
488
+ composition: BookingCopilotComposition,
489
+ maxBodyBytes: number,
490
+ negotiatedFeatures: readonly BookingSurfaceFeature[],
491
+ ): Promise<void> {
492
+ let turn: Exclude<BookingCopilotTurn, IngressTurn>
493
+ let browserRequestKey: string | undefined
494
+ let requestBinding: BookingIngressRequestBindingInput | undefined
495
+ let authorityFailureStatus = 403
496
+ try {
497
+ const parsed = JSON.parse(await readBody(req, maxBodyBytes)) as unknown
498
+ const valid = validateBookingSurface(parsed)
499
+ if (!valid.ok) { sendJson(res, 400, { error: { code: 'invalid_booking_surface_turn', details: valid.errors } }); return }
500
+ if ((parsed as BookingCopilotTurn).kind === 'user.turn.ingress') {
501
+ const ingress = parsed as IngressTurn
502
+ browserRequestKey = ingress.requestKey
503
+ if (composition.ingressMode !== 'bff-ingress-binding' || !composition.ingressBinding || !composition.principal) {
504
+ sendJson(res, 503, { error: { code: 'trusted_ingress_binding_required', mode: composition.ingressMode ?? 'bff-bound-turn-only', acceptedTurnKinds: [...BOOKING_COPILOT_ACCEPTED_TURN_KINDS] } })
505
+ return
506
+ }
507
+ const binding = await composition.ingressBinding.bind(ingress, composition.principal)
508
+ if (!validIngressBinding(binding, ingress)) { sendJson(res, 502, { error: { code: 'invalid_ingress_binding' } }); return }
509
+ if (!validIngressApprovalAuthority(ingress, binding)) { sendJson(res, 400, { error: { code: 'invalid_ingress_approval_authority' } }); return }
510
+ const proposedBoundWorkspace = {
511
+ ...ingress.workspace,
512
+ contextRef: binding.contextRef,
513
+ surface: binding.surface,
514
+ capabilities: { surface: binding.surface, allowedActions: [...binding.allowedActions] },
515
+ ...(binding.observableOrders !== undefined ? { observableOrders: binding.observableOrders.map((order) => ({ orderRef: order.orderRef, factRefs: [...order.factRefs] })) } : {}),
516
+ } as BookingWorkspaceSnapshot
517
+ const boundRequest = ingress.request.approval
518
+ ? { text: ingress.request.text, approval: { ...ingress.request.approval } }
519
+ : { text: ingress.request.text }
520
+ turn = { schemaVersion: BOOKING_SURFACE_SCHEMA_VERSION, kind: 'user.turn', taskId: binding.taskId, turnId: binding.turnId, workspace: proposedBoundWorkspace, request: boundRequest }
521
+ requestBinding = { requestKey: ingress.requestKey, principal: composition.principal, ...(ingress.taskHandle ? { taskHandle: ingress.taskHandle } : {}) }
522
+ authorityFailureStatus = 502
523
+ const boundValid = validateBookingSurface(turn)
524
+ if (!boundValid.ok) { sendJson(res, 502, { error: { code: 'invalid_ingress_binding', details: boundValid.errors } }); return }
525
+ } else {
526
+ const boundTurn = parsed as Exclude<BookingCopilotTurn, IngressTurn>
527
+ if (!validBoundWorkspaceAuthority(boundTurn.workspace)) {
528
+ sendJson(res, 403, { error: { code: 'invalid_bound_turn_authority' } })
529
+ return
530
+ }
531
+ turn = boundTurn
532
+ }
533
+ } catch (error) { const code = normalizeBookingErrorCode(error); sendJson(res, code === 'PAYLOAD_TOO_LARGE' ? 413 : 400, { error: { code } }); return }
534
+
535
+ let task: BookingCopilotTaskState; let fresh = false; let replayKey: string | undefined; let replayEvents: BookingSurfaceEvent[] | null = null; let activeDecisionKey: string | undefined
536
+ try {
537
+ const existingAtAdmission = turn.taskId ? composition.runtime.resumeTask(turn.taskId) : null
538
+ const narrowedWorkspace = narrowWorkspaceOptionalAuthority(turn.workspace, negotiatedFeatures, existingAtAdmission?.allowedActions)
539
+ if (!narrowedWorkspace) {
540
+ const code = authorityFailureStatus === 502 ? 'invalid_ingress_binding' : 'invalid_bound_turn_authority'
541
+ sendJson(res, authorityFailureStatus, { error: { code } })
542
+ return
543
+ }
544
+ turn = { ...turn, workspace: narrowedWorkspace }
545
+ fresh = turn.kind === 'action.receipt.continuation' ? false : existingAtAdmission === null
546
+ if (turn.kind === 'action.receipt.continuation') {
547
+ replayKey = `receipt:${turn.receipt.actionId}:${composition.runtime.receiptDigest(turn.receipt)}`
548
+ task = composition.runtime.continueWithReceipt(turn)
549
+ if (task.awaitingApproval) {
550
+ replayKey = composition.runtime.approvalPresentationRequestKey(turn.taskId)
551
+ }
552
+ replayEvents = composition.runtime.readDecisionBatch(turn.taskId, replayKey)
553
+ if (!replayEvents && task.phase === 'terminal') replayEvents = composition.runtime.terminalDecisionBatch(turn.taskId, replayKey)
554
+ } else {
555
+ const existing = existingAtAdmission
556
+ if (requestBinding && turn.kind === 'user.turn') composition.runtime.assertRequestBinding(browserRequestKey!, turn, requestBinding)
557
+ const requestReplayKey = existing && browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : undefined
558
+ const directReplayKey = existing && turn.kind === 'user.turn' && turn.turnId ? `turn:${existing.taskId}:${turn.turnId}` : undefined
559
+ const stableReplayKey = requestReplayKey ?? directReplayKey
560
+ const stableReplay = stableReplayKey ? composition.runtime.readDecisionBatch(existing!.taskId, stableReplayKey) : null
561
+ if (stableReplay) {
562
+ if (existing && turn.kind === 'user.turn' && directReplayKey) composition.runtime.assertTurnBinding(existing.taskId, turn)
563
+ task = existing!
564
+ replayKey = stableReplayKey
565
+ replayEvents = stableReplay
566
+ } else if (existing?.phase === 'terminal' || existing?.phase === 'error') {
567
+ throw new Error('task_terminal')
568
+ } else if (existing?.phase === 'waiting_receipt') {
569
+ if (turn.kind === 'user.turn' && turn.request.approval) composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval)
570
+ const workspace = turn.workspace as BookingWorkspaceSnapshot
571
+ if (workspace.contextRef !== existing.contextRef || workspace.surface !== existing.surface || workspace.revision !== existing.revision || workspace.capabilities.surface !== existing.surface || !sameCapabilities(workspace.capabilities.allowedActions, existing.allowedActions)) throw new Error('task_conflict:workspace_mismatch')
572
+ // A waiting task can only be replayed with the caller's stable turn
573
+ // identity. Without it, an identical sentence is not distinguishable
574
+ // from a new user turn and must not receive an old operation batch.
575
+ if (!turn.turnId || existing.lastTurnId !== turn.turnId) throw new Error('receipt_required')
576
+ replayKey = browserRequestKey ? `ingress:${existing.taskId}:${browserRequestKey}` : `turn:${existing.taskId}:${existing.lastTurnId ?? existing.userTurnCount}`
577
+ replayEvents = composition.runtime.readDecisionBatch(existing.taskId, replayKey)
578
+ if (!replayEvents) throw new Error('receipt_required')
579
+ task = existing
580
+ } else {
581
+ if (existing && turn.kind === 'user.turn' && turn.request.approval) composition.runtime.assertApprovalNotConsumed(existing.taskId, turn.request.approval)
582
+ task = composition.runtime.startTask(turn, requestBinding)
583
+ if (turn.kind === 'user.turn' && turn.taskId) {
584
+ replayKey = browserRequestKey ? `ingress:${task.taskId}:${browserRequestKey}` : `turn:${task.taskId}:${turn.turnId ?? task.userTurnCount}`
585
+ replayEvents = composition.runtime.readDecisionBatch(task.taskId, replayKey)
586
+ }
587
+ }
588
+ }
589
+ } catch (error) {
590
+ const code = normalizeBookingErrorCode(error)
591
+ console.error(JSON.stringify({ code, reason: extractBookingDispatchReason(error) }))
592
+ sendJson(res, 409, { error: { code } }); return
593
+ }
594
+
595
+ res.writeHead(200, { 'content-type': 'text/event-stream; charset=utf-8', 'cache-control': 'no-cache, no-transform', connection: 'keep-alive', 'x-content-type-options': 'nosniff', [BOOKING_SURFACE_VERSION_HEADER]: BOOKING_SURFACE_SCHEMA_VERSION, [BOOKING_SURFACE_SCHEMA_SHA256_HEADER]: BOOKING_SURFACE_SCHEMA_SHA256 })
596
+ res.flushHeaders()
597
+ try {
598
+ if (replayEvents) {
599
+ const progressEvents = composition.runtime.readProgressBatch(task.taskId, replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`) ?? []
600
+ for (const event of progressEvents) write(res, event)
601
+ if (progressEvents.length) {
602
+ flush(res)
603
+ await new Promise<void>((resolve) => setImmediate(resolve))
604
+ }
605
+ if (task.phase === 'terminal' || task.phase === 'error') releasePlannerSession(composition, task.taskId)
606
+ for (const event of replayEvents) write(res, event)
607
+ } else {
608
+ const requestKey = replayKey ?? `turn:${task.taskId}:${task.userTurnCount}`
609
+ activeDecisionKey = requestKey
610
+ // A concurrent identical request may arrive after the first request has
611
+ // committed progress but before its planner flight finishes. Re-serve
612
+ // the durable progress batch so it receives the same prefix instead of
613
+ // exposing a terminal-only tail.
614
+ const newlyPersistedProgress = composition.runtime.ensureProgressBatch(task.taskId, requestKey, fresh)
615
+ const progressEvents = newlyPersistedProgress.length > 0
616
+ ? newlyPersistedProgress
617
+ : (composition.runtime.readProgressBatch(task.taskId, requestKey) ?? [])
618
+ const approval = turn.kind === 'user.turn' ? turn.request.approval : undefined
619
+ const flightKey = JSON.stringify([task.taskId, approval ? `approval:${bookingDigest(approval)}` : requestKey])
620
+ // Register or join the decision flight before yielding after the progress
621
+ // flush. A duplicate that observed the durable progress must never miss
622
+ // an about-to-finish flight and pay for a second model invocation.
623
+ const decisionOutcome = runDecisionSingleFlight(composition, flightKey, async () => {
624
+ // Cover the complementary late-join case: the prior flight may have
625
+ // committed its exact batch and left the in-memory map between request
626
+ // admission and this work starting.
627
+ const durableDecision = composition.runtime.readDecisionBatch(task.taskId, requestKey)
628
+ if (durableDecision) return durableDecision
629
+ if (turn.kind === 'action.receipt.continuation' && task.awaitingApproval) {
630
+ return composition.runtime.applyDecisionBatch(task.taskId, requestKey, [composition.runtime.approvalQuestion(task.taskId)], false)
631
+ }
632
+ const sessions = sessionsFor(composition)
633
+ const session = sessions.get(task.taskId) ?? composition.plannerFactory(task)
634
+ sessions.set(task.taskId, session)
635
+ const decisions = await session.next({ turn, task: composition.runtime.resumeTask(task.taskId) ?? task })
636
+ const plannerDecisions = decisions.length ? decisions : [{ kind: 'error' as const, error: { code: 'PLANNER_NO_DECISION', message: 'Planner returned no typed decision.', retryable: false } }]
637
+ const events = composition.runtime.applyDecisionBatch(task.taskId, requestKey, plannerDecisions, fresh, { suppressProgressStatuses: true })
638
+ const updated = composition.runtime.resumeTask(task.taskId)
639
+ if (updated?.phase === 'terminal' || updated?.phase === 'error') releasePlannerSession(composition, task.taskId)
640
+ return events
641
+ }).then(
642
+ (events) => ({ ok: true as const, events }),
643
+ (error: unknown) => ({ ok: false as const, error }),
644
+ )
645
+ for (const event of progressEvents) write(res, event)
646
+ if (progressEvents.length) {
647
+ flush(res)
648
+ await new Promise<void>((resolve) => setImmediate(resolve))
649
+ }
650
+ const outcome = await decisionOutcome
651
+ if (!outcome.ok) throw outcome.error
652
+ for (const event of outcome.events) write(res, event)
653
+ }
654
+ } catch (error) {
655
+ writeTypedError(res, composition, task, error, activeDecisionKey, fresh)
656
+ const updated = composition.runtime.resumeTask(task.taskId)
657
+ if (updated?.phase === 'terminal' || updated?.phase === 'error') releasePlannerSession(composition, task.taskId)
658
+ } finally { res.end() }
659
+ }
660
+
661
+ export function startBookingCopilotServer(options: BookingCopilotServerOptions): Promise<BookingCopilotServerHandle> {
662
+ if (!options.apiKey) return Promise.reject(new Error('booking_copilot_api_key_required'))
663
+ if (options.artifactId !== undefined && !/^[0-9a-f]{40}$/.test(options.artifactId)) {
664
+ return Promise.reject(new Error('booking_copilot_artifact_id_invalid'))
665
+ }
666
+ if (!options.runtime || !options.plannerFactory) {
667
+ return Promise.reject(new Error('booking_copilot_runtime_and_planner_required'))
668
+ }
669
+ const hasIngressBinding = Boolean(options.ingressBinding)
670
+ const hasPrincipal = Boolean(options.principal)
671
+ if (hasIngressBinding !== hasPrincipal) {
672
+ return Promise.reject(new Error('booking_copilot_ingress_binding_pair_required'))
673
+ }
674
+ if (hasIngressBinding && (typeof options.ingressBinding?.bind !== 'function'
675
+ || typeof options.principal?.subject !== 'string' || options.principal.subject.length === 0 || options.principal.subject.length > 256
676
+ || typeof options.principal?.scope !== 'string' || options.principal.scope.length === 0 || options.principal.scope.length > 256)) {
677
+ return Promise.reject(new Error('booking_copilot_ingress_binding_invalid'))
678
+ }
679
+ const ingressMode: BookingCopilotIngressMode = options.ingressMode ?? (hasIngressBinding ? 'bff-ingress-binding' : 'bff-bound-turn-only')
680
+ if (!BOOKING_COPILOT_INGRESS_MODES.includes(ingressMode)) {
681
+ return Promise.reject(new Error('booking_copilot_ingress_mode_invalid'))
682
+ }
683
+ if (ingressMode === 'bff-ingress-binding' && !hasIngressBinding) {
684
+ return Promise.reject(new Error('booking_copilot_ingress_binding_required'))
685
+ }
686
+ if (ingressMode === 'bff-bound-turn-only' && hasIngressBinding) {
687
+ return Promise.reject(new Error('booking_copilot_ingress_mode_conflict'))
688
+ }
689
+ const composition: BookingCopilotComposition = {
690
+ runtime: options.runtime,
691
+ plannerFactory: options.plannerFactory,
692
+ ...(hasIngressBinding ? { ingressBinding: options.ingressBinding, principal: options.principal } : {}),
693
+ ingressMode,
694
+ }
695
+ const maxBodyBytes = options.maxBodyBytes ?? 1_000_000
696
+ const runningIdentity = runtimeIdentity()
697
+
698
+ const server = createServer((req, res) => {
699
+ void bookingCopilotTrafficHandler({
700
+ apiKey: options.apiKey,
701
+ composition,
702
+ maxBodyBytes,
703
+ ...(options.artifactId !== undefined ? { artifactId: options.artifactId } : {}),
704
+ ingressMode,
705
+ runningIdentity,
706
+ })(req, res)
707
+ })
708
+ let closePromise: Promise<void> | undefined
709
+
710
+ return new Promise((resolve, reject) => {
711
+ server.once('error', reject)
712
+ server.listen(options.port ?? 0, options.host ?? '127.0.0.1', () => {
713
+ server.off('error', reject)
714
+ const address = server.address()
715
+ const port = typeof address === 'object' && address ? address.port : (options.port ?? 0)
716
+ resolve({
717
+ server,
718
+ port,
719
+ close: () => {
720
+ if (closePromise) return closePromise
721
+ const trafficShutdown = shutdownBookingCopilotTraffic(composition)
722
+ closePromise = (async () => {
723
+ const listenerShutdown = new Promise<void>((done, fail) => server.close((error) => error ? fail(error) : done()))
724
+ const results = await Promise.allSettled([listenerShutdown, trafficShutdown])
725
+ const failures = results.flatMap((result) => result.status === 'rejected' ? [result.reason] : [])
726
+ if (failures.length > 0) throw new AggregateError(failures, 'booking_copilot_server_close_failed')
727
+ })()
728
+ return closePromise
729
+ },
730
+ })
731
+ })
732
+ })
733
+ }