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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (507) hide show
  1. package/README.md +172 -219
  2. package/README.zh-CN.md +222 -0
  3. package/bin/gotry-backend.js +53 -0
  4. package/bin/gotry-booking-copilot.js +53 -0
  5. package/bin/gotry-bootstrap.js +1744 -0
  6. package/bin/gotry-inner.js +733 -83
  7. package/bin/gotry-process-liveness.d.ts +27 -0
  8. package/bin/gotry-process-liveness.js +97 -0
  9. package/bin/gotry-runtime-resolution.d.ts +27 -0
  10. package/bin/gotry-runtime-resolution.js +49 -0
  11. package/bin/gotry.js +1 -1
  12. package/client/client.js +203 -0
  13. package/cordis.gotry-patch.yml +80 -56
  14. package/data/airline-airports.json +39 -0
  15. package/data/stations-12306-verify.json +524 -0
  16. package/dist/capabilities/agent-reach-deep.js +45 -122
  17. package/dist/capabilities/agent-reach.js +101 -146
  18. package/dist/capabilities/anything.js +135 -94
  19. package/dist/capabilities/artifacts.js +370 -0
  20. package/dist/capabilities/channel-health.js +131 -0
  21. package/dist/capabilities/channel-registry.js +256 -0
  22. package/dist/capabilities/doctor.js +546 -0
  23. package/dist/capabilities/effect.js +394 -0
  24. package/dist/capabilities/external-event.js +149 -0
  25. package/dist/capabilities/fact-log.js +55 -0
  26. package/dist/capabilities/flyai-config.js +337 -0
  27. package/dist/capabilities/flyai.js +947 -110
  28. package/dist/capabilities/fx-contract.js +293 -0
  29. package/dist/capabilities/geo-atlas.js +246 -0
  30. package/dist/capabilities/ground-transfer.js +509 -0
  31. package/dist/capabilities/hbcli.js +260 -123
  32. package/dist/capabilities/hotelbyte-transaction.js +194 -0
  33. package/dist/capabilities/incident-log.js +131 -61
  34. package/dist/capabilities/itinerary-artifact.js +190 -0
  35. package/dist/capabilities/itinerary-deck-artifact.js +194 -0
  36. package/dist/capabilities/itinerary-deck-export.js +339 -0
  37. package/dist/capabilities/lavish-local.js +924 -0
  38. package/dist/capabilities/model-override.js +38 -0
  39. package/dist/capabilities/opensky.js +54 -43
  40. package/dist/capabilities/resilience.js +191 -0
  41. package/dist/capabilities/session/action-cache.js +50 -44
  42. package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
  43. package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
  44. package/dist/capabilities/session/adapters/dida-portal.js +274 -0
  45. package/dist/capabilities/session/adapters/meituan-local.js +35 -54
  46. package/dist/capabilities/session/adapters/rail-12306.js +342 -0
  47. package/dist/capabilities/session/benchmark.js +189 -0
  48. package/dist/capabilities/session/bridge-ledger.js +224 -0
  49. package/dist/capabilities/session/extension-bridge.js +636 -0
  50. package/dist/capabilities/session/extension-channel.js +109 -0
  51. package/dist/capabilities/session/extension-distribution.js +230 -0
  52. package/dist/capabilities/session/extract.js +31 -21
  53. package/dist/capabilities/session/golden-score.js +114 -0
  54. package/dist/capabilities/session/health-watch.js +215 -0
  55. package/dist/capabilities/session/read-guard.js +75 -24
  56. package/dist/capabilities/session/static-flight-golden.js +150 -0
  57. package/dist/capabilities/session/transport.js +97 -83
  58. package/dist/capabilities/session/wizard.js +117 -0
  59. package/dist/capabilities/session-consent.js +178 -0
  60. package/dist/capabilities/session-login.js +170 -0
  61. package/dist/capabilities/session-search.js +635 -44
  62. package/dist/capabilities/spawn-bounded.js +203 -0
  63. package/dist/capabilities/sponsor-plugin.js +341 -0
  64. package/dist/capabilities/visa-policy.js +140 -0
  65. package/dist/capabilities/weather.js +243 -138
  66. package/dist/data/session-golden-20.json +25 -0
  67. package/dist/data/sf-golden-manifest.json +103 -0
  68. package/dist/data/sf-static-routes.json +91 -0
  69. package/dist/scripts/action-cache-tests.js +24 -59
  70. package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
  71. package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
  72. package/dist/scripts/agent-reach-deep-tests.js +30 -40
  73. package/dist/scripts/agent-reach-error-tests.js +354 -0
  74. package/dist/scripts/agent-reach-tests.js +26 -18
  75. package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
  76. package/dist/scripts/anything-tests.js +116 -60
  77. package/dist/scripts/artifact-client-contract-tests.js +220 -0
  78. package/dist/scripts/artifacts-capability-tests.js +727 -0
  79. package/dist/scripts/async-collect.js +50 -11
  80. package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
  81. package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
  82. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
  83. package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
  84. package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
  85. package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
  86. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
  87. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
  88. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
  89. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
  90. package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
  91. package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
  92. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
  93. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
  94. package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
  95. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
  96. package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
  97. package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
  98. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
  99. package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
  100. package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
  101. package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
  102. package/dist/scripts/booking-executor-tests.js +80 -0
  103. package/dist/scripts/booking-recovery-chain-tests.js +182 -0
  104. package/dist/scripts/booking-saga-tests.js +125 -0
  105. package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
  106. package/dist/scripts/bootstrap-tests.js +1479 -0
  107. package/dist/scripts/bridge-events-tests.js +239 -0
  108. package/dist/scripts/bridge-ledger-tests.js +428 -0
  109. package/dist/scripts/build-changelog.js +238 -0
  110. package/dist/scripts/build-metrics-report.js +331 -0
  111. package/dist/scripts/changelog-tests.js +118 -0
  112. package/dist/scripts/channel-probe-tests.js +103 -0
  113. package/dist/scripts/channel-probe.js +169 -0
  114. package/dist/scripts/channel-registry-tests.js +208 -0
  115. package/dist/scripts/companion-tests.js +25 -70
  116. package/dist/scripts/dida-cookie-probe.js +87 -0
  117. package/dist/scripts/dida-runner-stop-tests.js +311 -0
  118. package/dist/scripts/dida-sw-probe.js +114 -0
  119. package/dist/scripts/diff-test.js +17 -12
  120. package/dist/scripts/doctor-tests.js +564 -0
  121. package/dist/scripts/dsh-artifact-e2e.js +279 -0
  122. package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
  123. package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
  124. package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
  125. package/dist/scripts/dsh-runtime-closure.js +162 -0
  126. package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
  127. package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
  128. package/dist/scripts/dsh-target-closure-proof.js +178 -0
  129. package/dist/scripts/effect-tests.js +539 -0
  130. package/dist/scripts/engine-run.js +1 -3
  131. package/dist/scripts/engine-tests.js +16 -10
  132. package/dist/scripts/evaluation-cadence-tests.js +174 -0
  133. package/dist/scripts/evaluation-contract-tests.js +268 -0
  134. package/dist/scripts/extension-distribution-cli.js +39 -0
  135. package/dist/scripts/extension-distribution-tests.js +443 -0
  136. package/dist/scripts/extension-tests.js +988 -0
  137. package/dist/scripts/external-event-tests.js +350 -0
  138. package/dist/scripts/fact-gate-tests.js +869 -0
  139. package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
  140. package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
  141. package/dist/scripts/flight-malformed-tests.js +159 -0
  142. package/dist/scripts/flyai-setup-tests.js +228 -0
  143. package/dist/scripts/flyai-setup-tool-tests.js +54 -0
  144. package/dist/scripts/flyai-tests.js +692 -0
  145. package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
  146. package/dist/scripts/fx-contract-tests.js +201 -0
  147. package/dist/scripts/g5-guard-tests.js +131 -0
  148. package/dist/scripts/g5-guard.js +189 -0
  149. package/dist/scripts/gen-lunar-anchors.js +106 -0
  150. package/dist/scripts/geo-atlas-tests.js +283 -0
  151. package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
  152. package/dist/scripts/gotry-try-demo-tests.js +79 -0
  153. package/dist/scripts/gotry-try-demo.js +49 -0
  154. package/dist/scripts/ground-transfer-tests.js +956 -0
  155. package/dist/scripts/hbcli-e2e-tests.js +159 -0
  156. package/dist/scripts/hbcli-live-optin-tests.js +96 -0
  157. package/dist/scripts/hbcli-release-contract-tests.js +122 -0
  158. package/dist/scripts/hbcli-tests.js +121 -69
  159. package/dist/scripts/health-watch-cli.js +73 -0
  160. package/dist/scripts/hotel-date-gate-tests.js +496 -0
  161. package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
  162. package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
  163. package/dist/scripts/i18n-tests.js +41 -0
  164. package/dist/scripts/incident-tests.js +281 -87
  165. package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
  166. package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
  167. package/dist/scripts/issue-271-liveness-tests.js +569 -0
  168. package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
  169. package/dist/scripts/issue-289-model-retry-tests.js +441 -0
  170. package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
  171. package/dist/scripts/issue-338-home-city-e2e.js +577 -0
  172. package/dist/scripts/issue343-real-entry-e2e.js +160 -0
  173. package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
  174. package/dist/scripts/itinerary-artifact-tests.js +368 -0
  175. package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
  176. package/dist/scripts/itinerary-deck-export-tests.js +552 -0
  177. package/dist/scripts/itinerary-deck-tests.js +435 -0
  178. package/dist/scripts/itinerary-html-tests.js +426 -0
  179. package/dist/scripts/journey-tests.js +22 -27
  180. package/dist/scripts/kernel-manifest-evidence.js +109 -0
  181. package/dist/scripts/kernel-manifest-gate.js +126 -0
  182. package/dist/scripts/kernel-manifest-tests.js +212 -0
  183. package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
  184. package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
  185. package/dist/scripts/kernel-manifest.js +369 -0
  186. package/dist/scripts/lavish-local-tests.js +1226 -0
  187. package/dist/scripts/lavish-product-tools-tests.js +537 -0
  188. package/dist/scripts/ledger-repair-apply-tests.js +316 -0
  189. package/dist/scripts/ledger-repair-plan-tests.js +375 -0
  190. package/dist/scripts/ledger-tests.js +382 -246
  191. package/dist/scripts/ledger-workflow-crash.js +10 -20
  192. package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
  193. package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
  194. package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
  195. package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
  196. package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
  197. package/dist/scripts/memory-capture-tests.js +242 -61
  198. package/dist/scripts/memory-decay-tests.js +18 -52
  199. package/dist/scripts/memory-lifecycle-tests.js +657 -0
  200. package/dist/scripts/memory-lifecycle.js +196 -0
  201. package/dist/scripts/memory-metrics.js +24 -12
  202. package/dist/scripts/memory-value-report.js +631 -0
  203. package/dist/scripts/memory-value-tests.js +328 -0
  204. package/dist/scripts/metrics-report-tests.js +151 -0
  205. package/dist/scripts/model-override-e2e.js +137 -0
  206. package/dist/scripts/nightly-evidence-tests.js +161 -0
  207. package/dist/scripts/nightly-evidence.js +286 -0
  208. package/dist/scripts/nudge-digest.js +39 -28
  209. package/dist/scripts/observability-tests.js +381 -0
  210. package/dist/scripts/onboarding-tests.js +161 -0
  211. package/dist/scripts/opensky-check.js +18 -13
  212. package/dist/scripts/opensky-tests.js +28 -28
  213. package/dist/scripts/persona-surface-guard-tests.js +49 -0
  214. package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
  215. package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
  216. package/dist/scripts/price-drift-tests.js +340 -0
  217. package/dist/scripts/price-drift-watch.js +447 -0
  218. package/dist/scripts/probe-poi-tests.js +53 -45
  219. package/dist/scripts/product-metrics.js +483 -0
  220. package/dist/scripts/publish-preverify.js +110 -0
  221. package/dist/scripts/realtime-pricing-tests.js +76 -0
  222. package/dist/scripts/recall-tests.js +513 -0
  223. package/dist/scripts/replay-async.js +20 -18
  224. package/dist/scripts/replay-real.js +18 -18
  225. package/dist/scripts/replay.js +52 -82
  226. package/dist/scripts/session-attach-diagnose.js +20 -20
  227. package/dist/scripts/session-attach-poc.js +34 -37
  228. package/dist/scripts/session-benchmark.js +180 -0
  229. package/dist/scripts/session-dida-live-e2e.js +199 -0
  230. package/dist/scripts/session-extract-tests.js +14 -35
  231. package/dist/scripts/session-link-tests.js +478 -0
  232. package/dist/scripts/session-login.js +27 -46
  233. package/dist/scripts/session-tests.js +796 -96
  234. package/dist/scripts/session-zones-tests.js +358 -0
  235. package/dist/scripts/sf-live-benchmark.js +367 -0
  236. package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
  237. package/dist/scripts/sf-live-cli-tests.js +92 -0
  238. package/dist/scripts/sf-live-options.js +46 -0
  239. package/dist/scripts/sf-manifest-tests.js +30 -0
  240. package/dist/scripts/sf-manifest.js +33 -0
  241. package/dist/scripts/sf-soft-score-tests.js +67 -0
  242. package/dist/scripts/sf-summary-tests.js +324 -0
  243. package/dist/scripts/sf-summary.js +525 -0
  244. package/dist/scripts/share-tests.js +432 -0
  245. package/dist/scripts/skeleton-check.js +16 -14
  246. package/dist/scripts/skeleton-integration-test.js +28 -11
  247. package/dist/scripts/skills-contract-tests.js +51 -46
  248. package/dist/scripts/smoke-session-gate-tests.js +21 -0
  249. package/dist/scripts/smoke.js +690 -246
  250. package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
  251. package/dist/scripts/sponsor-reuse-tests.js +262 -0
  252. package/dist/scripts/state-cli-tests.js +226 -101
  253. package/dist/scripts/state-cli.js +550 -199
  254. package/dist/scripts/state-repair-cli.js +66 -0
  255. package/dist/scripts/state-repair-tests.js +140 -0
  256. package/dist/scripts/static-golden-tests.js +165 -0
  257. package/dist/scripts/time-eval-tests.js +210 -192
  258. package/dist/scripts/travel-timeline-tests.js +29 -93
  259. package/dist/scripts/turn-handoff-collect-tests.js +169 -0
  260. package/dist/scripts/turn-handoff-collect.js +161 -0
  261. package/dist/scripts/turn-policy-tests.js +55 -0
  262. package/dist/scripts/typed-contract-canary.js +176 -0
  263. package/dist/scripts/tz-resolver-sanity.js +50 -0
  264. package/dist/scripts/tz-resolver-tests.js +584 -0
  265. package/dist/scripts/unified-tests.js +23 -19
  266. package/dist/scripts/visa-policy-tests.js +62 -0
  267. package/dist/scripts/weather-tests.js +276 -41
  268. package/dist/scripts/wish-channel-gate-tests.js +54 -0
  269. package/dist/scripts/write-gate-tests.js +522 -0
  270. package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
  271. package/dist/scripts/z3-lifecycle-tests.js +170 -0
  272. package/dist/scripts/z3-race-repeat-tests.js +39 -0
  273. package/dist/scripts/z3-race-tests.js +41 -0
  274. package/dist/src/artifact-gate.js +829 -0
  275. package/dist/src/backend/kernel.js +91 -0
  276. package/dist/src/backend/modules/booking-copilot.js +92 -0
  277. package/dist/src/backend/modules/booking-executor.js +124 -0
  278. package/dist/src/backend/modules/session-search.js +551 -0
  279. package/dist/src/benchmark-agent-conformance.js +578 -0
  280. package/dist/src/benchmark-environment-bridge.js +515 -0
  281. package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
  282. package/dist/src/benchmark-tool-isolation.js +151 -0
  283. package/dist/src/bookable-facts.js +533 -0
  284. package/dist/src/booking-saga.js +97 -0
  285. package/dist/src/booking-surface/availability-policy.js +697 -0
  286. package/dist/src/booking-surface/booking-intent.js +43 -0
  287. package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
  288. package/dist/src/booking-surface/canonical-schema.js +116 -0
  289. package/dist/src/booking-surface/contracts.js +75 -0
  290. package/dist/src/booking-surface/dsh-planner.js +1854 -0
  291. package/dist/src/booking-surface/dsh-plugin.js +378 -0
  292. package/dist/src/booking-surface/dsh-worker.js +98 -0
  293. package/dist/src/booking-surface/error-codes.js +231 -0
  294. package/dist/src/booking-surface/index.js +9 -0
  295. package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
  296. package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
  297. package/dist/src/booking-surface/profile.js +24 -0
  298. package/dist/src/booking-surface/recovery-chain.js +367 -0
  299. package/dist/src/booking-surface/runtime.js +1968 -0
  300. package/dist/src/booking-surface/server.js +674 -0
  301. package/dist/src/booking-surface/startup.js +168 -0
  302. package/dist/src/booking-surface/validation.js +261 -0
  303. package/dist/src/bridge.js +20 -13
  304. package/dist/src/companions.js +48 -74
  305. package/dist/src/contracts.js +21 -43
  306. package/dist/src/dsh-llm.js +97 -111
  307. package/dist/src/engine.js +276 -193
  308. package/dist/src/evaluation-cadence.js +97 -0
  309. package/dist/src/evaluation-contracts.js +283 -0
  310. package/dist/src/flight-pack-adapter.js +54 -0
  311. package/dist/src/flight-pack-contract.js +2 -0
  312. package/dist/src/flyai-setup-tool.js +70 -0
  313. package/dist/src/gotry-backend.js +125 -0
  314. package/dist/src/hotel-date-gate.js +118 -0
  315. package/dist/src/i18n.js +88 -0
  316. package/dist/src/index.js +2227 -882
  317. package/dist/src/itinerary-deck.js +139 -0
  318. package/dist/src/itinerary-doc-shared.js +661 -0
  319. package/dist/src/itinerary-html.js +131 -0
  320. package/dist/src/journey.js +155 -108
  321. package/dist/src/lavish-tools.js +400 -0
  322. package/dist/src/ledger-repair-apply.js +538 -0
  323. package/dist/src/ledger-repair-plan.js +463 -0
  324. package/dist/src/loop.js +319 -160
  325. package/dist/src/memory-capture.js +180 -32
  326. package/dist/src/memory-decay.js +30 -14
  327. package/dist/src/memory-lifecycle.js +1310 -0
  328. package/dist/src/memory-utility.js +27 -40
  329. package/dist/src/mock-llm.js +36 -61
  330. package/dist/src/model.js +81 -36
  331. package/dist/src/realtime-pricing.js +103 -0
  332. package/dist/src/recall/card.js +74 -0
  333. package/dist/src/recall/evaluator.js +140 -0
  334. package/dist/src/recall/integration.js +17 -0
  335. package/dist/src/recall/tick.js +166 -0
  336. package/dist/src/session-link/plan-it-action.js +48 -0
  337. package/dist/src/session-link/session-link.js +248 -0
  338. package/dist/src/session-zones.js +507 -0
  339. package/dist/src/share/adapters.js +51 -0
  340. package/dist/src/share/share-consent.js +76 -0
  341. package/dist/src/share/share-deck.js +58 -0
  342. package/dist/src/share/share-token.js +146 -0
  343. package/dist/src/slot-spec.js +84 -106
  344. package/dist/src/state-ledger.js +435 -516
  345. package/dist/src/state-repair.js +299 -0
  346. package/dist/src/subagent-job-id-guard.js +58 -0
  347. package/dist/src/time-anchor.js +231 -33
  348. package/dist/src/tool-packet.js +20 -8
  349. package/dist/src/travel-slots.js +58 -35
  350. package/dist/src/travel-timeline.js +44 -63
  351. package/dist/src/turn-deadline.js +287 -0
  352. package/dist/src/turn-policy.js +118 -0
  353. package/dist/src/tz-resolver.js +143 -0
  354. package/dist/src/unified.js +535 -359
  355. package/dist/src/wish-pool.js +42 -12
  356. package/dist/src/write-gate.js +495 -0
  357. package/dist/src/z3-shared.js +284 -0
  358. package/extension/README.md +21 -0
  359. package/extension/README.zh-CN.md +21 -0
  360. package/extension/background.js +463 -0
  361. package/extension/content-bridge.js +270 -0
  362. package/extension/content-main.js +111 -0
  363. package/extension/manifest.json +60 -0
  364. package/package.json +407 -14
  365. package/schemas/booking.intent.schema.json +193 -0
  366. package/schemas/booking.surface.schema.json +2622 -0
  367. package/ts/capabilities/agent-reach-deep.ts +114 -0
  368. package/ts/capabilities/agent-reach.ts +198 -0
  369. package/ts/capabilities/anything.ts +255 -0
  370. package/ts/capabilities/artifacts.ts +428 -0
  371. package/ts/capabilities/fact-log.ts +54 -0
  372. package/ts/capabilities/flyai-config.ts +388 -0
  373. package/ts/capabilities/flyai.ts +1274 -0
  374. package/ts/capabilities/hbcli.ts +253 -71
  375. package/ts/capabilities/incident-log.ts +88 -59
  376. package/ts/capabilities/itinerary-artifact.ts +214 -0
  377. package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
  378. package/ts/capabilities/itinerary-deck-export.ts +378 -0
  379. package/ts/capabilities/opensky.ts +121 -0
  380. package/ts/capabilities/session/action-cache.ts +148 -0
  381. package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
  382. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  383. package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
  384. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  385. package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
  386. package/ts/capabilities/session/benchmark.ts +276 -0
  387. package/ts/capabilities/session/bridge-ledger.ts +333 -0
  388. package/ts/capabilities/session/extension-bridge.ts +794 -0
  389. package/ts/capabilities/session/extension-channel.ts +144 -0
  390. package/ts/capabilities/session/extension-distribution.ts +266 -0
  391. package/ts/capabilities/session/extract.ts +54 -0
  392. package/ts/capabilities/session/golden-score.ts +139 -0
  393. package/ts/capabilities/session/health-watch.ts +264 -0
  394. package/ts/capabilities/session/read-guard.ts +141 -0
  395. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  396. package/ts/capabilities/session/transport.ts +146 -0
  397. package/ts/capabilities/session/wizard.ts +140 -0
  398. package/ts/capabilities/session-consent.ts +211 -0
  399. package/ts/capabilities/session-login.ts +208 -0
  400. package/ts/capabilities/session-search.ts +893 -0
  401. package/ts/capabilities/weather.ts +330 -0
  402. package/ts/dsh-runtime/vendor/README.md +68 -0
  403. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  404. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
  405. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
  406. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
  407. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  408. package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
  409. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
  410. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  411. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
  412. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  413. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  414. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
  415. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  416. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
  417. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
  418. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
  419. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
  420. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  421. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  422. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
  423. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
  424. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  425. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  426. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  427. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  428. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
  429. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  430. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
  431. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  432. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  433. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
  434. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  435. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  436. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
  437. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
  438. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  439. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
  440. package/ts/package.json +293 -3
  441. package/ts/scripts/async-collect.ts +91 -0
  442. package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
  443. package/ts/scripts/skeleton-check.ts +1 -1
  444. package/ts/scripts/skeleton-integration-test.ts +23 -1
  445. package/ts/scripts/state-cli.ts +568 -0
  446. package/ts/scripts/turn-handoff-collect.ts +178 -0
  447. package/ts/src/artifact-gate.ts +991 -0
  448. package/ts/src/benchmark-agent-conformance.ts +619 -0
  449. package/ts/src/benchmark-environment-bridge.ts +538 -0
  450. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  451. package/ts/src/benchmark-tool-isolation.ts +166 -0
  452. package/ts/src/bookable-facts.ts +837 -0
  453. package/ts/src/booking-saga.ts +133 -0
  454. package/ts/src/booking-surface/availability-policy.ts +522 -0
  455. package/ts/src/booking-surface/booking-intent.ts +86 -0
  456. package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
  457. package/ts/src/booking-surface/canonical-schema.js +116 -0
  458. package/ts/src/booking-surface/contracts.ts +475 -0
  459. package/ts/src/booking-surface/dsh-planner.ts +1949 -0
  460. package/ts/src/booking-surface/dsh-plugin.js +378 -0
  461. package/ts/src/booking-surface/dsh-worker.ts +110 -0
  462. package/ts/src/booking-surface/error-codes.ts +241 -0
  463. package/ts/src/booking-surface/index.ts +9 -0
  464. package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
  465. package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
  466. package/ts/src/booking-surface/profile.ts +42 -0
  467. package/ts/src/booking-surface/recovery-chain.ts +409 -0
  468. package/ts/src/booking-surface/runtime.ts +1793 -0
  469. package/ts/src/booking-surface/server.ts +733 -0
  470. package/ts/src/booking-surface/startup.ts +201 -0
  471. package/ts/src/booking-surface/validation.ts +217 -0
  472. package/ts/src/companions.ts +113 -0
  473. package/ts/src/contracts.ts +13 -3
  474. package/ts/src/dsh-llm.ts +44 -11
  475. package/ts/src/flyai-setup-tool.ts +77 -0
  476. package/ts/src/index.ts +1948 -198
  477. package/ts/src/itinerary-deck.ts +189 -0
  478. package/ts/src/itinerary-doc-shared.ts +741 -0
  479. package/ts/src/itinerary-html.ts +171 -0
  480. package/ts/src/loop.ts +223 -25
  481. package/ts/src/memory-capture.ts +283 -0
  482. package/ts/src/memory-decay.ts +55 -0
  483. package/ts/src/memory-utility.ts +70 -0
  484. package/ts/src/mock-llm.ts +4 -8
  485. package/ts/src/model.ts +88 -2
  486. package/ts/src/recall/card.ts +106 -0
  487. package/ts/src/recall/evaluator.ts +170 -0
  488. package/ts/src/recall/integration.ts +35 -0
  489. package/ts/src/recall/tick.ts +213 -0
  490. package/ts/src/session-link/plan-it-action.ts +78 -0
  491. package/ts/src/session-link/session-link.ts +275 -0
  492. package/ts/src/share/adapters.ts +93 -0
  493. package/ts/src/share/share-consent.ts +105 -0
  494. package/ts/src/share/share-deck.ts +85 -0
  495. package/ts/src/share/share-token.ts +166 -0
  496. package/ts/src/slot-spec.ts +172 -0
  497. package/ts/src/state-ledger.ts +912 -0
  498. package/ts/src/subagent-job-id-guard.ts +72 -0
  499. package/ts/src/time-anchor.ts +338 -0
  500. package/ts/src/tool-packet.ts +32 -0
  501. package/ts/src/travel-slots.ts +219 -0
  502. package/ts/src/travel-timeline.ts +83 -0
  503. package/ts/src/turn-deadline.ts +366 -0
  504. package/ts/src/turn-policy.ts +154 -0
  505. package/ts/src/unified.ts +354 -88
  506. package/ts/src/wish-pool.ts +79 -0
  507. package/dist/scripts/session-attach-wait.js +0 -41
@@ -0,0 +1,784 @@
1
+ /**
2
+ * BFF-only typed HTTP/SSE seam proof.
3
+ *
4
+ * Run with Node 24 from the repository root:
5
+ * npx --yes --package=node@24 --package=tsx --call \
6
+ * 'tsx ts/scripts/booking-copilot-server-proof-tests.ts'
7
+ */
8
+ import assert from 'node:assert/strict';
9
+ import { mkdtempSync, rmSync } from 'node:fs';
10
+ import { request as httpRequest } from 'node:http';
11
+ import { tmpdir } from 'node:os';
12
+ import { join } from 'node:path';
13
+ import { ensureLedger } from '../src/state-ledger.js';
14
+ import { closeBackend, createBackendServer } from '../src/backend/kernel.js';
15
+ import { startBookingCopilotModule } from '../src/backend/modules/booking-copilot.js';
16
+ import { BookingCopilotTaskRuntime } from '../src/booking-surface/runtime.js';
17
+ import { startBookingCopilotServer } from '../src/booking-surface/server.js';
18
+ import { BOOKING_SURFACE_FEATURES, BOOKING_SURFACE_SCHEMA_SHA256, BOOKING_SURFACE_SCHEMA_VERSION } from '../src/booking-surface/contracts.js';
19
+ const API_KEY = 'server-to-server-key';
20
+ const SCHEMA_VERSION = BOOKING_SURFACE_SCHEMA_VERSION;
21
+ const SCHEMA_SHA256 = BOOKING_SURFACE_SCHEMA_SHA256;
22
+ const ARTIFACT_ID = '1111111111111111111111111111111111111111';
23
+ const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-server-'));
24
+ const ledger = ensureLedger(stateRoot);
25
+ let nextId = 0;
26
+ const runtime = new BookingCopilotTaskRuntime(ledger, {
27
+ idFactory: (prefix) => `${prefix}-${++nextId}`,
28
+ now: () => '2026-08-30T12:00:00.000Z',
29
+ });
30
+ let factoryCalls = 0;
31
+ let sessionTurns = 0;
32
+ let sessionCloses = 0;
33
+ const plannerFactory = (initialTask) => {
34
+ factoryCalls += 1;
35
+ assert.equal(initialTask.taskId, 'task-http-1');
36
+ return {
37
+ async next({ task }) {
38
+ sessionTurns += 1;
39
+ if (sessionTurns === 1) {
40
+ return [{
41
+ kind: 'operation',
42
+ action: {
43
+ schemaVersion: 'booking.surface',
44
+ kind: 'search.run',
45
+ actionId: 'action-http-1',
46
+ contextRef: task.contextRef,
47
+ expectedRevision: task.revision,
48
+ reason: 'Run the authoritative workspace search',
49
+ factRefs: [],
50
+ input: {},
51
+ },
52
+ intent: { schemaVersion: 'booking.intent.v1', target: 'search.results' },
53
+ }];
54
+ }
55
+ return [
56
+ {
57
+ kind: 'explanation',
58
+ explanation: { text: '{"kind":"search.run","input":{}}', factRefs: [] },
59
+ },
60
+ {
61
+ kind: 'terminal',
62
+ terminal: { status: 'completed', summary: 'Stopped at the requested search result.', factRefs: [] },
63
+ },
64
+ ];
65
+ },
66
+ async close() { sessionCloses += 1; },
67
+ };
68
+ };
69
+ const serverHandle = await startBookingCopilotServer({
70
+ apiKey: API_KEY,
71
+ runtime,
72
+ plannerFactory,
73
+ artifactId: ARTIFACT_ID,
74
+ });
75
+ const endpoint = `http://127.0.0.1:${serverHandle.port}/a2a/booking-copilot/turn`;
76
+ const expectedProbe = {
77
+ schemaVersion: SCHEMA_VERSION,
78
+ schemaSha256: SCHEMA_SHA256,
79
+ status: 'ready',
80
+ ingressMode: 'bff-bound-turn-only',
81
+ acceptedTurnKinds: ['user.turn', 'action.receipt.continuation'],
82
+ features: [...BOOKING_SURFACE_FEATURES],
83
+ };
84
+ const runtimeReport = process.report?.getReport?.();
85
+ const runtimeGlibcVersion = runtimeReport?.header?.glibcVersionRuntime ?? '';
86
+ const runtimeReleaseTuple = `${process.platform}-${process.arch}-${runtimeGlibcVersion ? 'glibc' : 'unknown'}`;
87
+ await assert.rejects(startBookingCopilotServer({
88
+ apiKey: API_KEY,
89
+ runtime,
90
+ plannerFactory,
91
+ artifactId: 'not-a-commit',
92
+ }), /booking_copilot_artifact_id_invalid/);
93
+ for (const path of ['/healthz', '/status']) {
94
+ const probeUrl = `http://127.0.0.1:${serverHandle.port}${path}`;
95
+ for (const authorization of [undefined, 'Bearer wrong-key']) {
96
+ const rejected = await fetch(probeUrl, authorization ? { headers: { authorization } } : undefined);
97
+ assert.equal(rejected.status, 401, `${path} rejects ${authorization ? 'wrong' : 'missing'} bearer`);
98
+ }
99
+ const authorized = await fetch(probeUrl, { headers: { authorization: `Bearer ${API_KEY}` } });
100
+ assert.equal(authorized.status, 200, `${path} accepts deployment bearer`);
101
+ assert.equal(authorized.headers.get('x-booking-surface-version'), SCHEMA_VERSION, `${path} returns canonical schema version header`);
102
+ assert.equal(authorized.headers.get('x-booking-surface-schema-sha256'), SCHEMA_SHA256, `${path} returns canonical schema hash header`);
103
+ assert.equal(authorized.headers.get('x-booking-surface-features'), BOOKING_SURFACE_FEATURES.join(','), `${path} advertises additive features independently of the schema hash`);
104
+ assert.equal(authorized.headers.get('x-gotry-artifact-id'), ARTIFACT_ID, `${path} returns the running release identity`);
105
+ assert.equal(authorized.headers.get('x-gotry-node-version'), process.version, `${path} returns the actual Node runtime`);
106
+ assert.equal(authorized.headers.get('x-gotry-node-modules-abi'), process.versions.modules, `${path} returns the actual native ABI`);
107
+ assert.equal(authorized.headers.get('x-gotry-release-tuple'), runtimeReleaseTuple, `${path} returns the actual runtime tuple`);
108
+ assert.equal(authorized.headers.get('x-gotry-glibc-version'), runtimeGlibcVersion || null, `${path} returns glibc only when present`);
109
+ assert.deepEqual(await authorized.json(), expectedProbe, `${path} exposes only typed readiness contract`);
110
+ }
111
+ const workspace = {
112
+ schemaVersion: 'booking.surface',
113
+ contextRef: 'ctx-http-1',
114
+ surface: 'tenant',
115
+ revision: 0,
116
+ locale: 'zh-CN',
117
+ currency: 'AED',
118
+ searchDraft: {},
119
+ results: { status: 'idle' },
120
+ visibleHotels: [],
121
+ loadedOffers: [],
122
+ shortlistedOfferRefs: [],
123
+ capabilities: {
124
+ surface: 'tenant',
125
+ allowedActions: ['search.run'],
126
+ },
127
+ };
128
+ const userTurn = {
129
+ schemaVersion: 'booking.surface',
130
+ kind: 'user.turn',
131
+ taskId: 'task-http-1',
132
+ turnId: 'http-turn-1',
133
+ workspace,
134
+ request: { text: '执行当前搜索' },
135
+ };
136
+ const post = (body, authorization = `Bearer ${API_KEY}`, schemaHeaders = {
137
+ 'x-booking-surface-version': SCHEMA_VERSION,
138
+ 'x-booking-surface-schema-sha256': SCHEMA_SHA256,
139
+ }) => fetch(endpoint, {
140
+ method: 'POST',
141
+ headers: { authorization, 'content-type': 'application/json', accept: 'text/event-stream', ...schemaHeaders },
142
+ body: JSON.stringify(body),
143
+ });
144
+ const turnFor = (taskId, contextRef) => ({
145
+ ...userTurn,
146
+ taskId,
147
+ turnId: `${taskId}-turn-1`,
148
+ workspace: { ...workspace, contextRef },
149
+ });
150
+ // Optional fields/actions are admitted only from the fresh fleet feature
151
+ // intersection supplied by the authenticated BFF. An old compatible BFF may
152
+ // keep sending the base matrix; GoTry narrows it without failing the turn.
153
+ const featureRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-feature-negotiation-'));
154
+ const featureLedger = ensureLedger(featureRoot);
155
+ const featureRuntime = new BookingCopilotTaskRuntime(featureLedger);
156
+ const featureTasks = [];
157
+ const featureServer = await startBookingCopilotServer({
158
+ apiKey: API_KEY,
159
+ runtime: featureRuntime,
160
+ plannerFactory: (initialTask) => {
161
+ const orders = initialTask.workspaceSnapshot?.observableOrders ?? [];
162
+ featureTasks.push({ actions: [...initialTask.allowedActions], orderCount: orders.length });
163
+ const decision = initialTask.allowedActions.includes('order.observe') && orders[0]
164
+ ? {
165
+ kind: 'operation',
166
+ intent: { schemaVersion: 'booking.intent.v1', target: 'order.observed' },
167
+ action: {
168
+ schemaVersion: 'booking.surface', kind: 'order.observe',
169
+ actionId: `action-${initialTask.taskId}`, contextRef: initialTask.contextRef,
170
+ expectedRevision: initialTask.revision, reason: 'Observe the trusted existing order.',
171
+ factRefs: [...orders[0].factRefs], input: { orderRef: orders[0].orderRef },
172
+ },
173
+ }
174
+ : {
175
+ kind: 'operation',
176
+ intent: { schemaVersion: 'booking.intent.v1', target: 'search.results' },
177
+ action: {
178
+ schemaVersion: 'booking.surface', kind: 'search.run',
179
+ actionId: `action-${initialTask.taskId}`, contextRef: initialTask.contextRef,
180
+ expectedRevision: initialTask.revision, reason: 'Run the authoritative search.', factRefs: [], input: {},
181
+ },
182
+ };
183
+ return { async next() { return [decision]; } };
184
+ },
185
+ });
186
+ try {
187
+ const optionalWorkspace = {
188
+ ...workspace,
189
+ contextRef: 'ctx-feature-old-bff',
190
+ observableOrders: [{ orderRef: 'order-feature-1', factRefs: ['order:feature:1'] }],
191
+ capabilities: { surface: 'tenant', allowedActions: ['search.run', 'order.observe'] },
192
+ };
193
+ const optionalEndpoint = `http://127.0.0.1:${featureServer.port}/a2a/booking-copilot/turn`;
194
+ const optionalHeaders = {
195
+ authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream',
196
+ 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256,
197
+ };
198
+ const oldBffResponse = await fetch(optionalEndpoint, {
199
+ method: 'POST', headers: optionalHeaders,
200
+ body: JSON.stringify({ ...userTurn, taskId: 'task-feature-old-bff', turnId: 'turn-feature-old-bff', workspace: optionalWorkspace }),
201
+ });
202
+ assert.equal(oldBffResponse.status, 200, 'an old same-line BFF base matrix is narrowed instead of rejected');
203
+ assert.match(await oldBffResponse.text(), /"kind":"search.run"/);
204
+ assert.deepEqual(featureTasks[0], { actions: ['search.run'], orderCount: 0 }, 'missing feature evidence strips the optional action and authority projection');
205
+ const negotiatedResponse = await fetch(optionalEndpoint, {
206
+ method: 'POST', headers: { ...optionalHeaders, 'x-booking-surface-features': 'future-unknown,trusted-order-observation-v1' },
207
+ body: JSON.stringify({ ...userTurn, taskId: 'task-feature-negotiated', turnId: 'turn-feature-negotiated', workspace: { ...optionalWorkspace, contextRef: 'ctx-feature-negotiated' } }),
208
+ });
209
+ assert.equal(negotiatedResponse.status, 200);
210
+ assert.match(await negotiatedResponse.text(), /"kind":"order.observe"/);
211
+ assert.deepEqual(featureTasks[1], { actions: ['search.run', 'order.observe'], orderCount: 1 }, 'known fleet-intersection feature enables only its optional action and projection');
212
+ }
213
+ finally {
214
+ await featureServer.close();
215
+ featureLedger.close();
216
+ rmSync(featureRoot, { recursive: true, force: true });
217
+ }
218
+ const noAuth = await fetch(endpoint, {
219
+ method: 'POST',
220
+ headers: { 'content-type': 'application/json' },
221
+ body: JSON.stringify(userTurn),
222
+ });
223
+ assert.equal(noAuth.status, 401, 'server-to-server key is fail-closed');
224
+ const missingSchemaHeaders = await post(userTurn, `Bearer ${API_KEY}`, {});
225
+ assert.equal(missingSchemaHeaders.status, 409, 'the breaking-version header is required');
226
+ const malformedSchemaHash = await post(userTurn, `Bearer ${API_KEY}`, {
227
+ 'x-booking-surface-version': SCHEMA_VERSION,
228
+ 'x-booking-surface-schema-sha256': 'not-a-sha256',
229
+ });
230
+ assert.equal(malformedSchemaHash.status, 400, 'a supplied schema hash remains a well-formed diagnostic identity');
231
+ const directIngress = await post({
232
+ schemaVersion: 'booking.surface',
233
+ kind: 'user.turn.ingress',
234
+ requestKey: 'browser-request-1',
235
+ surfaceHint: 'tenant',
236
+ workspace: {
237
+ schemaVersion: 'booking.surface', revision: 0, locale: 'zh-CN', currency: 'AED',
238
+ searchDraft: {}, results: { status: 'idle' }, visibleHotels: [], loadedOffers: [], shortlistedOfferRefs: [],
239
+ },
240
+ request: { text: 'browser must use same-origin BFF' },
241
+ });
242
+ assert.equal(directIngress.status, 503, 'GoTry server rejects unbound browser ingress');
243
+ assert.equal((await directIngress.json()).error.code, 'trusted_ingress_binding_required');
244
+ const tokenSmuggle = await post({ ...userTurn, portalToken: 'ST:must-not-cross-boundary' });
245
+ assert.equal(tokenSmuggle.status, 400, 'strict planner turn rejects portal token fields');
246
+ const versionOnly = await post(userTurn, `Bearer ${API_KEY}`, {
247
+ 'x-booking-surface-version': SCHEMA_VERSION,
248
+ });
249
+ assert.equal(versionOnly.status, 200, 'the compatibility version is sufficient when the diagnostic hash is omitted');
250
+ assert.equal(versionOnly.headers.get('x-booking-surface-schema-sha256'), SCHEMA_SHA256, 'the server still advertises its exact local schema hash');
251
+ const versionOnlyBody = await versionOnly.text();
252
+ const first = await post(userTurn, `Bearer ${API_KEY}`, {
253
+ 'x-booking-surface-version': SCHEMA_VERSION,
254
+ 'x-booking-surface-schema-sha256': '0'.repeat(64),
255
+ });
256
+ assert.equal(first.status, 200);
257
+ assert.equal(first.headers.get('x-booking-surface-schema-sha256'), SCHEMA_SHA256, 'a compatible peer hash is accepted and the local canonical hash is advertised back');
258
+ assert.match(String(first.headers.get('content-type')), /text\/event-stream/);
259
+ assert.equal(first.headers.get('x-booking-surface-version'), SCHEMA_VERSION);
260
+ assert.equal(first.headers.get('x-booking-surface-schema-sha256'), SCHEMA_SHA256);
261
+ function parseSse(body) {
262
+ return body.trim().split('\n\n').map((frame) => {
263
+ const data = frame.split('\n').find((line) => line.startsWith('data: '));
264
+ assert.ok(data, `SSE frame has data: ${frame}`);
265
+ return JSON.parse(data.slice('data: '.length));
266
+ });
267
+ }
268
+ /** Open an SSE stream without undici's body buffering, so the proof can
269
+ * observe the durable progress prefix while the planner remains gated. */
270
+ function openSseRequest(url, init) {
271
+ return new Promise((resolve, reject) => {
272
+ const request = httpRequest(new URL(url), { method: init.method, headers: init.headers });
273
+ request.once('error', reject);
274
+ request.end(init.body);
275
+ request.once('response', (response) => {
276
+ let text = '';
277
+ let resolveFirst;
278
+ let rejectFirst;
279
+ let firstSettled = false;
280
+ const firstEvents = new Promise((firstResolve, firstReject) => {
281
+ resolveFirst = firstResolve;
282
+ rejectFirst = firstReject;
283
+ });
284
+ const body = new Promise((bodyResolve, bodyReject) => {
285
+ response.setEncoding('utf8');
286
+ response.on('data', (chunk) => {
287
+ text += chunk;
288
+ if (firstSettled)
289
+ return;
290
+ const events = [];
291
+ for (const frame of text.split('\n\n')) {
292
+ const data = frame.split('\n').find((line) => line.startsWith('data: '));
293
+ if (!data)
294
+ continue;
295
+ try {
296
+ events.push(JSON.parse(data.slice('data: '.length)));
297
+ }
298
+ catch { /* incomplete frame */ }
299
+ }
300
+ if (events.length >= 2) {
301
+ firstSettled = true;
302
+ resolveFirst(events);
303
+ }
304
+ });
305
+ response.on('end', () => {
306
+ if (!firstSettled)
307
+ rejectFirst(new Error('sse_ended_before_progress'));
308
+ bodyResolve(text);
309
+ });
310
+ response.on('error', (error) => {
311
+ if (!firstSettled)
312
+ rejectFirst(error);
313
+ bodyReject(error);
314
+ });
315
+ });
316
+ resolve({ statusCode: response.statusCode ?? 0, firstEvents, body });
317
+ });
318
+ });
319
+ }
320
+ const firstBody = await first.text();
321
+ assert.equal(firstBody, versionOnlyBody, 'a different same-line diagnostic hash replays the same typed result');
322
+ const firstEvents = parseSse(firstBody);
323
+ assert.deepEqual(firstEvents.map((event) => event.kind), ['status', 'status', 'operation', 'status']);
324
+ assert.equal(firstEvents[3].kind === 'status' ? firstEvents[3].status : '', 'waiting_receipt', 'the operation leaves the task durably waiting for its receipt');
325
+ assert.ok(firstEvents.every((event) => event.contextRef === workspace.contextRef), 'every typed event carries BFF-minted contextRef');
326
+ const operation = firstEvents[2];
327
+ assert.equal(operation?.kind, 'operation');
328
+ assert.equal(factoryCalls, 1);
329
+ assert.equal(sessionTurns, 1);
330
+ const receiptTurn = {
331
+ schemaVersion: 'booking.surface',
332
+ kind: 'action.receipt.continuation',
333
+ taskId: 'task-http-1',
334
+ workspace: {
335
+ ...workspace,
336
+ revision: 1,
337
+ results: { status: 'ready', resultCount: 2, searchSessionRef: 'search-http-1' },
338
+ },
339
+ receipt: {
340
+ schemaVersion: 'booking.surface',
341
+ kind: 'action.receipt',
342
+ actionId: 'action-http-1',
343
+ contextRef: workspace.contextRef,
344
+ status: 'applied',
345
+ revision: 1,
346
+ observation: { kind: 'search.state', searchSessionRef: 'search-http-1', resultCount: 2 },
347
+ resultContract: { outcome: 'complete', hardCriteriaMet: true, factRefs: [], gapCodes: [], blockers: [], relaxationsApplied: [] },
348
+ },
349
+ };
350
+ const second = await post(receiptTurn);
351
+ assert.equal(second.status, 200);
352
+ const secondEvents = parseSse(await second.text());
353
+ assert.deepEqual(secondEvents.map((event) => event.kind), ['status', 'explanation', 'terminal']);
354
+ assert.equal(factoryCalls, 1, 'one task reuses one planner session across receipt continuations');
355
+ assert.equal(sessionTurns, 2);
356
+ assert.equal(sessionCloses, 1, 'a durable terminal decision releases the task-scoped planner session');
357
+ assert.ok(!secondEvents.some((event) => event.kind === 'operation'), 'JSON-looking explanation text is not parsed into an operation');
358
+ assert.ok(secondEvents[0].sequence > firstEvents.at(-1).sequence, 'SSE sequence is monotonic across turns');
359
+ // Progress is its own durable batch. It must reach the client while the
360
+ // planner is still gated, and the final batch must not repeat submitted or
361
+ // working. The same request key is single-flight and replays progress plus
362
+ // final events byte-for-byte.
363
+ const progressRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-server-progress-'));
364
+ const progressLedger = ensureLedger(progressRoot);
365
+ const progressRuntime = new BookingCopilotTaskRuntime(progressLedger, { contextRefFactory: () => 'ctx-progress' });
366
+ let progressPlannerCalls = 0;
367
+ let releaseProgressPlanner;
368
+ const progressPlannerGate = new Promise((resolve) => { releaseProgressPlanner = resolve; });
369
+ const progressServer = await startBookingCopilotServer({
370
+ apiKey: API_KEY,
371
+ runtime: progressRuntime,
372
+ plannerFactory: () => ({
373
+ async next() {
374
+ progressPlannerCalls += 1;
375
+ await progressPlannerGate;
376
+ return [{ kind: 'terminal', terminal: { status: 'completed', summary: 'Progress gate released.', factRefs: [] } }];
377
+ },
378
+ async close() { },
379
+ }),
380
+ });
381
+ const progressEndpoint = `http://127.0.0.1:${progressServer.port}/a2a/booking-copilot/turn`;
382
+ const progressRequestKey = 'turn:task-progress-gated:task-progress-gated-turn-1';
383
+ const progressRequest = {
384
+ method: 'POST',
385
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
386
+ body: JSON.stringify(turnFor('task-progress-gated', 'ctx-progress')),
387
+ };
388
+ const progressResponse = await openSseRequest(progressEndpoint, progressRequest);
389
+ const progressPrefix = await Promise.race([
390
+ progressResponse.firstEvents,
391
+ new Promise((_, reject) => setTimeout(() => reject(new Error('sse_progress_prefix_timeout')), 2_000)),
392
+ ]);
393
+ assert.equal(progressResponse.statusCode, 200);
394
+ assert.deepEqual(progressPrefix.slice(0, 2).map((event) => event.kind), ['status', 'status'], 'client receives submitted/working before planner release');
395
+ assert.deepEqual(progressPrefix.slice(0, 2).map((event) => event.kind === 'status' ? event.status : ''), ['submitted', 'working'], 'first SSE frames carry typed status fields');
396
+ assert.equal(progressPrefix.some((event) => event.kind === 'terminal'), false, 'client receives no terminal before planner release');
397
+ assert.equal(progressPlannerCalls, 1, 'planner is admitted only after durable submitted/working progress is flushed');
398
+ const concurrentProgressResponse = await openSseRequest(progressEndpoint, progressRequest);
399
+ const concurrentProgressPrefix = await Promise.race([
400
+ concurrentProgressResponse.firstEvents,
401
+ new Promise((_, reject) => setTimeout(() => reject(new Error('concurrent_sse_progress_prefix_timeout')), 2_000)),
402
+ ]);
403
+ assert.deepEqual(concurrentProgressPrefix.slice(0, 2).map((event) => event.kind === 'status' ? event.status : ''), ['submitted', 'working'], 'duplicate request receives durable progress before the first planner flight is released');
404
+ releaseProgressPlanner();
405
+ const progressBody = await progressResponse.body;
406
+ const progressEvents = parseSse(progressBody);
407
+ assert.deepEqual(progressEvents.map((event) => event.kind), ['status', 'status', 'terminal']);
408
+ assert.deepEqual(progressEvents.map((event) => event.sequence), [1, 2, 3], 'durable progress and final events use monotonic task sequences');
409
+ assert.equal(concurrentProgressResponse.statusCode, 200);
410
+ assert.equal(await concurrentProgressResponse.body, progressBody, 'concurrent identical requests receive the same progress and final batch');
411
+ assert.equal(progressPlannerCalls, 1, 'concurrent identical requests enter the planner once');
412
+ const progressReplay = await fetch(progressEndpoint, progressRequest);
413
+ assert.equal(await progressReplay.text(), progressBody, 'replay combines the exact durable progress and final batches');
414
+ assert.equal(progressPlannerCalls, 1, 'final replay does not re-enter the planner');
415
+ await progressServer.close();
416
+ // The same late-join guard applies when the durable final batch is an error;
417
+ // retrying the HTTP request must replay it without buying another model call.
418
+ const errorProgressRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-server-progress-error-'));
419
+ const errorProgressLedger = ensureLedger(errorProgressRoot);
420
+ const errorProgressRuntime = new BookingCopilotTaskRuntime(errorProgressLedger, { contextRefFactory: () => 'ctx-progress-error' });
421
+ let errorProgressPlannerCalls = 0;
422
+ let releaseErrorProgressPlanner;
423
+ const errorProgressPlannerGate = new Promise((resolve) => { releaseErrorProgressPlanner = resolve; });
424
+ const errorProgressServer = await startBookingCopilotServer({
425
+ apiKey: API_KEY,
426
+ runtime: errorProgressRuntime,
427
+ plannerFactory: () => ({
428
+ async next() {
429
+ errorProgressPlannerCalls += 1;
430
+ await errorProgressPlannerGate;
431
+ return [{ kind: 'error', error: { code: 'PLANNER_PROVIDER_UNAVAILABLE', message: 'fixture text is replaced', retryable: true } }];
432
+ },
433
+ async close() { },
434
+ }),
435
+ });
436
+ const errorProgressEndpoint = `http://127.0.0.1:${errorProgressServer.port}/a2a/booking-copilot/turn`;
437
+ const errorProgressRequest = {
438
+ method: 'POST',
439
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
440
+ body: JSON.stringify(turnFor('task-progress-error', 'ctx-progress-error')),
441
+ };
442
+ const firstErrorProgressResponse = await openSseRequest(errorProgressEndpoint, errorProgressRequest);
443
+ await Promise.race([
444
+ firstErrorProgressResponse.firstEvents,
445
+ new Promise((_, reject) => setTimeout(() => reject(new Error('error_sse_progress_prefix_timeout')), 2_000)),
446
+ ]);
447
+ const concurrentErrorProgressResponse = await openSseRequest(errorProgressEndpoint, errorProgressRequest);
448
+ await Promise.race([
449
+ concurrentErrorProgressResponse.firstEvents,
450
+ new Promise((_, reject) => setTimeout(() => reject(new Error('concurrent_error_sse_progress_prefix_timeout')), 2_000)),
451
+ ]);
452
+ releaseErrorProgressPlanner();
453
+ const errorProgressBody = await firstErrorProgressResponse.body;
454
+ assert.deepEqual(parseSse(errorProgressBody).map((event) => event.kind), ['status', 'status', 'error']);
455
+ assert.equal(await concurrentErrorProgressResponse.body, errorProgressBody, 'concurrent identical error requests receive the same progress and final batch');
456
+ assert.equal(errorProgressPlannerCalls, 1, 'concurrent identical error requests enter the planner once');
457
+ await errorProgressServer.close();
458
+ errorProgressLedger.close();
459
+ rmSync(errorProgressRoot, { recursive: true, force: true });
460
+ async function assertImmediateDecisionFailureIsHandled(name, failure, expectedCode) {
461
+ const failureRoot = mkdtempSync(join(tmpdir(), `gotry-booking-server-${name}-`));
462
+ const failureLedger = ensureLedger(failureRoot);
463
+ const contextRef = `ctx-${name}`;
464
+ const taskId = `task-${name}`;
465
+ const failureRuntime = new BookingCopilotTaskRuntime(failureLedger, { contextRefFactory: () => contextRef });
466
+ let plannerCalls = 0;
467
+ const failureServer = await startBookingCopilotServer({
468
+ apiKey: API_KEY,
469
+ runtime: failureRuntime,
470
+ plannerFactory: () => ({
471
+ async next() {
472
+ plannerCalls += 1;
473
+ if (failure === 'planner-throw')
474
+ throw new Error('raw provider failure must stay private');
475
+ return [{
476
+ kind: 'operation',
477
+ intent: { schemaVersion: 'booking.intent.v1', target: 'results.refined' },
478
+ action: {
479
+ schemaVersion: 'booking.surface',
480
+ kind: 'results.view.patch',
481
+ actionId: `action-${name}`,
482
+ contextRef,
483
+ expectedRevision: 0,
484
+ reason: 'Exercise immediate apply failure handling',
485
+ factRefs: [],
486
+ input: { patch: { sort: 'price_asc' } },
487
+ },
488
+ }];
489
+ },
490
+ async close() { },
491
+ }),
492
+ });
493
+ const failureEndpoint = `http://127.0.0.1:${failureServer.port}/a2a/booking-copilot/turn`;
494
+ const failureRequest = {
495
+ method: 'POST',
496
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
497
+ body: JSON.stringify(turnFor(taskId, contextRef)),
498
+ };
499
+ const response = await fetch(failureEndpoint, failureRequest);
500
+ const body = await response.text();
501
+ const events = parseSse(body);
502
+ assert.deepEqual(events.map((event) => event.kind), ['status', 'status', 'error'], `${name} remains a typed SSE response after progress`);
503
+ const lastEvent = events.at(-1);
504
+ assert.equal(lastEvent?.kind === 'error' ? lastEvent.error.code : '', expectedCode);
505
+ assert.doesNotMatch(body, /typed runtime boundary/, 'the reported opaque boundary message is retired');
506
+ const replay = await fetch(failureEndpoint, failureRequest);
507
+ assert.equal(await replay.text(), body, `${name} replays the exact durable error batch`);
508
+ assert.equal(plannerCalls, 1, `${name} enters the planner once`);
509
+ await failureServer.close();
510
+ failureLedger.close();
511
+ rmSync(failureRoot, { recursive: true, force: true });
512
+ }
513
+ await assertImmediateDecisionFailureIsHandled('immediate-planner-throw', 'planner-throw', 'PLANNER_FAILED');
514
+ await assertImmediateDecisionFailureIsHandled('immediate-apply-throw', 'apply-throw', 'UNSUPPORTED_ACTION');
515
+ // A crash after the progress commit but before the planner's final decision
516
+ // reuses the same progress event identities after runtime reconstruction.
517
+ const crashProgressRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-server-progress-crash-'));
518
+ const crashProgressLedger = ensureLedger(crashProgressRoot);
519
+ const crashProgressRuntime = new BookingCopilotTaskRuntime(crashProgressLedger, { contextRefFactory: () => 'ctx-progress-crash' });
520
+ const crashTurn = { ...turnFor('task-progress-crash', 'ctx-progress-crash'), request: { text: 'crash after progress' } };
521
+ crashProgressRuntime.startTask(crashTurn);
522
+ const crashRequestKey = 'turn:task-progress-crash:1';
523
+ const crashProgress = crashProgressRuntime.ensureProgressBatch('task-progress-crash', crashRequestKey, true);
524
+ const crashProgressIdentity = crashProgress.map((event) => [event.eventId, event.sequence]);
525
+ crashProgressLedger.close();
526
+ const recoveredProgressLedger = ensureLedger(crashProgressRoot);
527
+ const recoveredProgressRuntime = new BookingCopilotTaskRuntime(recoveredProgressLedger, { contextRefFactory: () => 'ctx-progress-crash' });
528
+ assert.deepEqual(recoveredProgressRuntime.readProgressBatch('task-progress-crash', crashRequestKey)?.map((event) => [event.eventId, event.sequence]), crashProgressIdentity, 'crash recovery reuses durable progress event identities');
529
+ const recoveredFinal = recoveredProgressRuntime.applyDecisionBatch('task-progress-crash', crashRequestKey, [{ kind: 'terminal', terminal: { status: 'completed', summary: 'Recovered after progress.', factRefs: [] } }], false, { suppressProgressStatuses: true });
530
+ const recoveredCombined = [...(recoveredProgressRuntime.readProgressBatch('task-progress-crash', crashRequestKey) ?? []), ...recoveredFinal];
531
+ assert.deepEqual(recoveredCombined.map((event) => event.sequence), [1, 2, 3], 'crash recovery continues the sequence without duplicating progress');
532
+ recoveredProgressLedger.close();
533
+ rmSync(crashProgressRoot, { recursive: true, force: true });
534
+ progressLedger.close();
535
+ const reopenedProgressLedger = ensureLedger(progressRoot);
536
+ const progressRestartRuntime = new BookingCopilotTaskRuntime(reopenedProgressLedger, { contextRefFactory: () => 'ctx-progress' });
537
+ assert.deepEqual(progressRestartRuntime.readProgressBatch('task-progress-gated', progressRequestKey)?.map((event) => [event.eventId, event.sequence]), progressEvents.slice(0, 2).map((event) => [event.eventId, event.sequence]), 'progress batch survives a runtime restart with stable event identities');
538
+ reopenedProgressLedger.close();
539
+ rmSync(progressRoot, { recursive: true, force: true });
540
+ // A slow subprocess/session close is cleanup work, not part of the user-visible
541
+ // SSE critical path. The response must be writable while close is still gated.
542
+ let slowCloseStarted = false;
543
+ let releaseSlowClose;
544
+ const slowCloseGate = new Promise((resolve) => { releaseSlowClose = resolve; });
545
+ const slowRuntime = new BookingCopilotTaskRuntime(ledger, {
546
+ idFactory: (prefix) => `${prefix}-slow-${++nextId}`,
547
+ now: () => '2026-08-30T12:02:00.000Z',
548
+ });
549
+ const slowServer = await startBookingCopilotServer({
550
+ apiKey: API_KEY,
551
+ runtime: slowRuntime,
552
+ plannerFactory: () => ({
553
+ async next() {
554
+ return [{ kind: 'terminal', terminal: { status: 'completed', summary: 'Slow cleanup proof.', factRefs: [] } }];
555
+ },
556
+ async close() { slowCloseStarted = true; await slowCloseGate; },
557
+ }),
558
+ });
559
+ const slowEndpoint = `http://127.0.0.1:${slowServer.port}/a2a/booking-copilot/turn`;
560
+ const slowResponse = await fetch(slowEndpoint, {
561
+ method: 'POST',
562
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
563
+ body: JSON.stringify(turnFor('task-slow-close', 'ctx-slow-close')),
564
+ signal: AbortSignal.timeout(1_000),
565
+ });
566
+ const slowStartedAt = Date.now();
567
+ const slowBody = await slowResponse.text();
568
+ const slowElapsedMs = Date.now() - slowStartedAt;
569
+ assert.equal(slowResponse.status, 200);
570
+ assert.match(slowBody, /event: terminal/);
571
+ assert.ok(slowElapsedMs < 500, `SSE response is not blocked by slow cleanup (${slowElapsedMs}ms)`);
572
+ await new Promise((resolve) => setImmediate(resolve));
573
+ assert.equal(slowCloseStarted, true, 'terminal cleanup is scheduled before the SSE response ends');
574
+ releaseSlowClose();
575
+ await slowServer.close();
576
+ // A failed detached close remains sticky and is surfaced during shutdown;
577
+ // the request still returns its durable terminal event immediately. Session
578
+ // close is not assumed retryable because the real DSH session caches its close
579
+ // promise and a second call must not launder the first cleanup failure.
580
+ let failingCloseAttempts = 0;
581
+ const cleanupErrorLogs = [];
582
+ const originalConsoleError = console.error;
583
+ console.error = (...args) => { cleanupErrorLogs.push(args.map(String).join(' ')); };
584
+ const retryRuntime = new BookingCopilotTaskRuntime(ledger, {
585
+ idFactory: (prefix) => `${prefix}-retry-${++nextId}`,
586
+ now: () => '2026-08-30T12:03:00.000Z',
587
+ });
588
+ const retryServer = await startBookingCopilotServer({
589
+ apiKey: API_KEY,
590
+ runtime: retryRuntime,
591
+ plannerFactory: () => ({
592
+ async next() {
593
+ return [{ kind: 'terminal', terminal: { status: 'completed', summary: 'Retry cleanup proof.', factRefs: [] } }];
594
+ },
595
+ async close() {
596
+ failingCloseAttempts += 1;
597
+ if (failingCloseAttempts === 1)
598
+ throw new Error('injected cleanup failure');
599
+ },
600
+ }),
601
+ });
602
+ const retryResponse = await fetch(`http://127.0.0.1:${retryServer.port}/a2a/booking-copilot/turn`, {
603
+ method: 'POST',
604
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
605
+ body: JSON.stringify(turnFor('task-retry-close', 'ctx-retry-close')),
606
+ });
607
+ assert.equal(retryResponse.status, 200);
608
+ assert.match(await retryResponse.text(), /event: terminal/);
609
+ await new Promise((resolve) => setImmediate(resolve));
610
+ assert.equal(failingCloseAttempts, 1, 'failed close is recorded without delaying the response');
611
+ await assert.rejects(retryServer.close(), /booking_copilot_server_close_failed/, 'shutdown surfaces the sticky detached cleanup failure');
612
+ assert.equal(failingCloseAttempts, 1, 'shutdown never retries a non-retryable session close');
613
+ console.error = originalConsoleError;
614
+ assert.ok(cleanupErrorLogs.some((line) => line.includes('PLANNER_SESSION_CLOSE_FAILED')), 'cleanup failure is reported with a closed machine-readable code');
615
+ // Shutdown stops new turns while an accepted planner flight drains. A late
616
+ // request must not create a second planner execution after shutdown begins.
617
+ let flightStarted = false;
618
+ let releaseFlight;
619
+ const flightGate = new Promise((resolve) => { releaseFlight = resolve; });
620
+ let flightPlannerCalls = 0;
621
+ let flightStartedResolve;
622
+ const flightStartedSignal = new Promise((resolve) => { flightStartedResolve = resolve; });
623
+ const flightRuntime = new BookingCopilotTaskRuntime(ledger, {
624
+ idFactory: (prefix) => `${prefix}-flight-${++nextId}`,
625
+ now: () => '2026-08-30T12:04:00.000Z',
626
+ });
627
+ const flightServer = await startBookingCopilotServer({
628
+ apiKey: API_KEY,
629
+ runtime: flightRuntime,
630
+ plannerFactory: () => ({
631
+ async next() {
632
+ flightPlannerCalls += 1;
633
+ flightStarted = true;
634
+ flightStartedResolve();
635
+ await flightGate;
636
+ return [{ kind: 'terminal', terminal: { status: 'completed', summary: 'Shutdown flight proof.', factRefs: [] } }];
637
+ },
638
+ async close() { },
639
+ }),
640
+ });
641
+ const flightEndpoint = `http://127.0.0.1:${flightServer.port}/a2a/booking-copilot/turn`;
642
+ const flightResponsePromise = fetch(flightEndpoint, {
643
+ method: 'POST',
644
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
645
+ body: JSON.stringify(turnFor('task-shutdown-flight', 'ctx-shutdown-flight')),
646
+ });
647
+ await flightStartedSignal;
648
+ assert.equal(flightStarted, true);
649
+ const shutdownPromise = flightServer.close();
650
+ const lateResponse = await fetch(flightEndpoint, {
651
+ method: 'POST',
652
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
653
+ body: JSON.stringify(turnFor('task-late-after-shutdown', 'ctx-late-after-shutdown')),
654
+ }).catch(() => null);
655
+ if (lateResponse)
656
+ assert.equal(lateResponse.status, 503, 'shutdown rejects late turns before planner dispatch');
657
+ assert.equal(flightPlannerCalls, 1, 'late turn never executes a second planner flight');
658
+ releaseFlight();
659
+ const flightResponse = await flightResponsePromise;
660
+ assert.equal(flightResponse.status, 200);
661
+ assert.match(await flightResponse.text(), /event: terminal/);
662
+ await shutdownPromise;
663
+ // The deployed shape is the unified gotry-backend module, whose listener is
664
+ // owned by the kernel. Module shutdown must stop admission, drain an accepted
665
+ // turn and close its task session before the process planner/ledger.
666
+ const moduleStateRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-module-'));
667
+ let releaseModuleFlight;
668
+ const moduleFlightGate = new Promise((resolve) => { releaseModuleFlight = resolve; });
669
+ let moduleFlightStarted;
670
+ const moduleFlightSignal = new Promise((resolve) => { moduleFlightStarted = resolve; });
671
+ const moduleCloseOrder = [];
672
+ const bookingModule = await startBookingCopilotModule({
673
+ GOTRY_BOOKING_COPILOT_API_KEY: API_KEY,
674
+ GOTRY_BOOKING_COPILOT_STATE_ROOT: moduleStateRoot,
675
+ GOTRY_BOOKING_COPILOT_PORT: '0',
676
+ }, {
677
+ ensureLedger,
678
+ async createPlanner() {
679
+ return {
680
+ plannerFactory: () => ({
681
+ async next() {
682
+ moduleFlightStarted();
683
+ await moduleFlightGate;
684
+ return [{ kind: 'terminal', terminal: { status: 'completed', summary: 'Unified module shutdown proof.', factRefs: [] } }];
685
+ },
686
+ async close() { moduleCloseOrder.push('session'); },
687
+ }),
688
+ async close() { moduleCloseOrder.push('planner'); },
689
+ };
690
+ },
691
+ });
692
+ const backend = await createBackendServer({ modules: [bookingModule], port: 0 });
693
+ const moduleEndpoint = `http://127.0.0.1:${backend.port}/a2a/booking-copilot/turn`;
694
+ let backendClosed = false;
695
+ try {
696
+ const moduleResponsePromise = fetch(moduleEndpoint, {
697
+ method: 'POST',
698
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
699
+ body: JSON.stringify(turnFor('task-module-shutdown', 'ctx-module-shutdown')),
700
+ });
701
+ await moduleFlightSignal;
702
+ const moduleShutdown = closeBackend(backend, [bookingModule]);
703
+ const moduleLateResponse = await fetch(moduleEndpoint, {
704
+ method: 'POST',
705
+ headers: { authorization: `Bearer ${API_KEY}`, 'content-type': 'application/json', accept: 'text/event-stream', 'x-booking-surface-version': SCHEMA_VERSION, 'x-booking-surface-schema-sha256': SCHEMA_SHA256 },
706
+ body: JSON.stringify(turnFor('task-module-late', 'ctx-module-late')),
707
+ });
708
+ assert.equal(moduleLateResponse.status, 503, 'unified module rejects new turns while its kernel listener is still draining');
709
+ releaseModuleFlight();
710
+ const moduleResponse = await moduleResponsePromise;
711
+ assert.equal(moduleResponse.status, 200);
712
+ assert.match(await moduleResponse.text(), /event: terminal/);
713
+ await moduleShutdown;
714
+ backendClosed = true;
715
+ assert.deepEqual(moduleCloseOrder, ['session', 'planner'], 'unified module drains task sessions before closing the process planner');
716
+ }
717
+ finally {
718
+ releaseModuleFlight();
719
+ if (!backendClosed)
720
+ await closeBackend(backend, [bookingModule]).catch(() => undefined);
721
+ rmSync(moduleStateRoot, { recursive: true, force: true });
722
+ }
723
+ await serverHandle.close();
724
+ const preRestartRuntime = new BookingCopilotTaskRuntime(ledger, {
725
+ idFactory: (prefix) => `${prefix}-probe-${++nextId}`,
726
+ now: () => '2026-08-30T12:01:00.000Z',
727
+ });
728
+ const recovered = preRestartRuntime.resumeTask('task-http-1');
729
+ assert.equal(recovered?.lastReceipt?.actionId, 'action-http-1', 'new server process seam restores task from last typed receipt');
730
+ assert.equal(recovered?.phase, 'terminal', 'the completed task stays durably terminal across restart');
731
+ const restarted = await startBookingCopilotServer({
732
+ apiKey: API_KEY,
733
+ runtime: preRestartRuntime,
734
+ plannerFactory: (initialTask) => {
735
+ assert.equal(initialTask.taskId, 'task-http-2');
736
+ return { async next() { return [{ kind: 'terminal', terminal: { status: 'stopped', summary: 'Recovered.', factRefs: [] } }]; } };
737
+ },
738
+ });
739
+ const restartedEndpoint = `http://127.0.0.1:${restarted.port}/a2a/booking-copilot/turn`;
740
+ const afterRestartResponse = await fetch(restartedEndpoint, {
741
+ method: 'POST',
742
+ headers: {
743
+ authorization: `Bearer ${API_KEY}`,
744
+ 'content-type': 'application/json',
745
+ accept: 'text/event-stream',
746
+ 'x-booking-surface-version': SCHEMA_VERSION,
747
+ 'x-booking-surface-schema-sha256': SCHEMA_SHA256,
748
+ },
749
+ body: JSON.stringify({ ...userTurn, taskId: 'task-http-2', turnId: 'http-restart-turn-1', workspace: { ...workspace, contextRef: 'ctx-http-2' } }),
750
+ });
751
+ assert.equal(afterRestartResponse.status, 200);
752
+ const restartEvents = parseSse(await afterRestartResponse.text());
753
+ assert.deepEqual(restartEvents.map((event) => event.kind), ['status', 'status', 'terminal']);
754
+ assert.deepEqual(restartEvents.map((event) => event.sequence), [1, 2, 3], 'the restarted process allocates fresh task-scoped sequences after recovering the ledger');
755
+ await restarted.close();
756
+ ledger.close();
757
+ rmSync(stateRoot, { recursive: true, force: true });
758
+ // The unified gotry-backend mounts the module, not the standalone startup: the
759
+ // planner deadline must be forwarded here too, or a deployment's declared value
760
+ // never reaches the serving path (measured: env had 45000, the metric still
761
+ // timed out at 12s because only startup.ts applied it).
762
+ {
763
+ const deadlineStateRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-deadline-'));
764
+ let seenPlannerTimeout;
765
+ const deadlineModule = await startBookingCopilotModule({
766
+ GOTRY_BOOKING_COPILOT_API_KEY: API_KEY,
767
+ GOTRY_BOOKING_COPILOT_STATE_ROOT: deadlineStateRoot,
768
+ GOTRY_BOOKING_COPILOT_PORT: '0',
769
+ GOTRY_BOOKING_COPILOT_PLANNER_TIMEOUT_MS: '45000',
770
+ }, {
771
+ ensureLedger,
772
+ async createPlanner(options) {
773
+ seenPlannerTimeout = options.turnTimeoutMs;
774
+ return {
775
+ plannerFactory: () => ({ async next() { return []; }, async close() { } }),
776
+ async close() { },
777
+ };
778
+ },
779
+ });
780
+ assert.equal(seenPlannerTimeout, 45000, 'the unified module forwards the planner deadline from the environment');
781
+ await deadlineModule.close();
782
+ rmSync(deadlineStateRoot, { recursive: true, force: true });
783
+ }
784
+ console.log('BOOKING COPILOT SERVER PROOF: BFF auth/typed SSE/task session/restart/no-token/no-text-action OK');