@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.17

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 (238) hide show
  1. package/README.md +51 -13
  2. package/README.zh-CN.md +26 -12
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +357 -8
  5. package/bin/gotry-inner.js +442 -60
  6. package/bin/gotry-runtime-resolution.d.ts +27 -0
  7. package/bin/gotry-runtime-resolution.js +50 -0
  8. package/bin/gotry.js +1 -1
  9. package/cordis.gotry-patch.yml +15 -1
  10. package/data/airline-airports.json +39 -0
  11. package/dist/capabilities/agent-reach-deep.js +1 -1
  12. package/dist/capabilities/agent-reach.js +1 -1
  13. package/dist/capabilities/anything.js +1 -1
  14. package/dist/capabilities/artifacts.js +1 -1
  15. package/dist/capabilities/effect.js +293 -0
  16. package/dist/capabilities/fact-log.js +41 -0
  17. package/dist/capabilities/flyai.js +20 -6
  18. package/dist/capabilities/hbcli.js +43 -1
  19. package/dist/capabilities/incident-log.js +1 -1
  20. package/dist/capabilities/model-override.js +18 -0
  21. package/dist/capabilities/opensky.js +1 -1
  22. package/dist/capabilities/resilience.js +90 -0
  23. package/dist/capabilities/session/action-cache.js +1 -1
  24. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  25. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  26. package/dist/capabilities/session/benchmark.js +2 -1
  27. package/dist/capabilities/session/extension-bridge.js +358 -0
  28. package/dist/capabilities/session/extension-channel.js +93 -0
  29. package/dist/capabilities/session/extension-distribution.js +234 -0
  30. package/dist/capabilities/session/extract.js +1 -1
  31. package/dist/capabilities/session/golden-score.js +92 -0
  32. package/dist/capabilities/session/health-watch.js +216 -0
  33. package/dist/capabilities/session/read-guard.js +1 -1
  34. package/dist/capabilities/session/static-flight-golden.js +137 -0
  35. package/dist/capabilities/session/transport.js +1 -1
  36. package/dist/capabilities/session/wizard.js +147 -0
  37. package/dist/capabilities/session-consent.js +1 -1
  38. package/dist/capabilities/session-login.js +83 -1
  39. package/dist/capabilities/session-search.js +111 -2
  40. package/dist/capabilities/weather.js +168 -46
  41. package/dist/data/session-golden-20.json +25 -0
  42. package/dist/data/sf-golden-manifest.json +102 -0
  43. package/dist/data/sf-static-routes.json +91 -0
  44. package/dist/scripts/action-cache-tests.js +1 -1
  45. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  46. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  47. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  48. package/dist/scripts/agent-reach-tests.js +1 -1
  49. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  50. package/dist/scripts/anything-tests.js +1 -1
  51. package/dist/scripts/async-collect.js +1 -1
  52. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  53. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  54. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  55. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  56. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  57. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  58. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  59. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  60. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  61. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  62. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  63. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  64. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  65. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  66. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  67. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  68. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  69. package/dist/scripts/booking-saga-tests.js +1 -1
  70. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  71. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  72. package/dist/scripts/bootstrap-tests.js +59 -3
  73. package/dist/scripts/build-changelog.js +279 -0
  74. package/dist/scripts/changelog-tests.js +165 -0
  75. package/dist/scripts/companion-tests.js +1 -1
  76. package/dist/scripts/diff-test.js +1 -1
  77. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  78. package/dist/scripts/dsh-runtime-closure.js +150 -0
  79. package/dist/scripts/effect-tests.js +474 -0
  80. package/dist/scripts/engine-run.js +1 -1
  81. package/dist/scripts/engine-tests.js +1 -1
  82. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  83. package/dist/scripts/evaluation-contract-tests.js +574 -0
  84. package/dist/scripts/extension-distribution-cli.js +35 -0
  85. package/dist/scripts/extension-distribution-tests.js +341 -0
  86. package/dist/scripts/extension-tests.js +573 -0
  87. package/dist/scripts/fact-gate-tests.js +318 -0
  88. package/dist/scripts/flyai-tests.js +1 -1
  89. package/dist/scripts/hbcli-e2e-tests.js +180 -0
  90. package/dist/scripts/hbcli-tests.js +1 -1
  91. package/dist/scripts/health-watch-cli.js +51 -0
  92. package/dist/scripts/i18n-tests.js +1 -1
  93. package/dist/scripts/incident-tests.js +1 -1
  94. package/dist/scripts/journey-tests.js +1 -1
  95. package/dist/scripts/ledger-tests.js +1 -1
  96. package/dist/scripts/ledger-workflow-crash.js +1 -1
  97. package/dist/scripts/memory-capture-tests.js +1 -1
  98. package/dist/scripts/memory-decay-tests.js +1 -1
  99. package/dist/scripts/memory-metrics.js +1 -1
  100. package/dist/scripts/memory-value-report.js +1 -1
  101. package/dist/scripts/model-override-e2e.js +176 -0
  102. package/dist/scripts/nightly-evidence-tests.js +67 -4
  103. package/dist/scripts/nightly-evidence.js +59 -3
  104. package/dist/scripts/nudge-digest.js +1 -1
  105. package/dist/scripts/onboarding-tests.js +186 -0
  106. package/dist/scripts/opensky-check.js +1 -1
  107. package/dist/scripts/opensky-tests.js +1 -1
  108. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  109. package/dist/scripts/price-drift-tests.js +441 -0
  110. package/dist/scripts/price-drift-watch.js +491 -0
  111. package/dist/scripts/probe-poi-tests.js +1 -1
  112. package/dist/scripts/product-metrics.js +1 -1
  113. package/dist/scripts/publish-preverify.js +40 -4
  114. package/dist/scripts/realtime-pricing-tests.js +1 -1
  115. package/dist/scripts/replay-async.js +1 -1
  116. package/dist/scripts/replay-real.js +1 -1
  117. package/dist/scripts/replay.js +1 -1
  118. package/dist/scripts/session-attach-diagnose.js +1 -1
  119. package/dist/scripts/session-attach-poc.js +1 -1
  120. package/dist/scripts/session-benchmark.js +1 -1
  121. package/dist/scripts/session-extract-tests.js +1 -1
  122. package/dist/scripts/session-login.js +1 -1
  123. package/dist/scripts/session-tests.js +70 -20
  124. package/dist/scripts/sf-live-benchmark.js +338 -0
  125. package/dist/scripts/sf-live-cli-tests.js +21 -0
  126. package/dist/scripts/sf-soft-score-tests.js +108 -0
  127. package/dist/scripts/sf-summary.js +93 -0
  128. package/dist/scripts/skeleton-check.js +1 -1
  129. package/dist/scripts/skeleton-integration-test.js +1 -1
  130. package/dist/scripts/skills-contract-tests.js +1 -1
  131. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  132. package/dist/scripts/smoke.js +154 -33
  133. package/dist/scripts/state-cli-tests.js +1 -1
  134. package/dist/scripts/state-cli.js +1 -1
  135. package/dist/scripts/static-golden-tests.js +299 -0
  136. package/dist/scripts/time-eval-tests.js +1 -1
  137. package/dist/scripts/travel-timeline-tests.js +1 -1
  138. package/dist/scripts/unified-tests.js +1 -1
  139. package/dist/scripts/weather-tests.js +694 -44
  140. package/dist/scripts/z3-race-tests.js +1 -1
  141. package/dist/src/artifact-gate.js +310 -0
  142. package/dist/src/benchmark-agent-conformance.js +370 -0
  143. package/dist/src/benchmark-environment-bridge.js +384 -0
  144. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  145. package/dist/src/benchmark-tool-isolation.js +124 -0
  146. package/dist/src/bookable-facts.js +349 -0
  147. package/dist/src/booking-saga.js +1 -1
  148. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  149. package/dist/src/booking-surface/canonical-schema.js +113 -0
  150. package/dist/src/booking-surface/contracts-v2.js +89 -0
  151. package/dist/src/booking-surface/contracts.js +46 -0
  152. package/dist/src/booking-surface/dsh-planner.js +453 -0
  153. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  154. package/dist/src/booking-surface/error-codes.js +94 -0
  155. package/dist/src/booking-surface/index.js +15 -0
  156. package/dist/src/booking-surface/profile.js +68 -0
  157. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  158. package/dist/src/booking-surface/runtime.js +351 -0
  159. package/dist/src/booking-surface/server-v2.js +334 -0
  160. package/dist/src/booking-surface/server.js +302 -0
  161. package/dist/src/booking-surface/startup.js +159 -0
  162. package/dist/src/booking-surface/validation-v2.js +319 -0
  163. package/dist/src/booking-surface/validation.js +809 -0
  164. package/dist/src/bridge.js +1 -1
  165. package/dist/src/companions.js +1 -1
  166. package/dist/src/contracts.js +1 -1
  167. package/dist/src/dsh-llm.js +1 -1
  168. package/dist/src/engine.js +1 -1
  169. package/dist/src/evaluation-cadence.js +234 -0
  170. package/dist/src/evaluation-contracts.js +906 -0
  171. package/dist/src/i18n.js +1 -1
  172. package/dist/src/index.js +234 -60
  173. package/dist/src/journey.js +1 -1
  174. package/dist/src/loop.js +1 -1
  175. package/dist/src/memory-capture.js +1 -1
  176. package/dist/src/memory-decay.js +1 -1
  177. package/dist/src/memory-utility.js +1 -1
  178. package/dist/src/mock-llm.js +1 -1
  179. package/dist/src/model.js +1 -1
  180. package/dist/src/realtime-pricing.js +14 -7
  181. package/dist/src/slot-spec.js +1 -1
  182. package/dist/src/state-ledger.js +2 -1
  183. package/dist/src/time-anchor.js +1 -1
  184. package/dist/src/tool-budget.js +136 -0
  185. package/dist/src/tool-packet.js +1 -1
  186. package/dist/src/travel-slots.js +1 -1
  187. package/dist/src/travel-timeline.js +1 -1
  188. package/dist/src/unified.js +1 -1
  189. package/dist/src/wish-pool.js +1 -1
  190. package/dist/src/z3-shared.js +1 -1
  191. package/extension/README.md +50 -0
  192. package/extension/background.js +160 -0
  193. package/extension/content-bridge.js +41 -0
  194. package/extension/content-main.js +62 -0
  195. package/extension/manifest.json +32 -0
  196. package/package.json +291 -12
  197. package/schemas/booking.surface.v1.schema.json +927 -0
  198. package/schemas/booking.surface.v2.schema.json +61 -0
  199. package/ts/capabilities/fact-log.ts +54 -0
  200. package/ts/capabilities/flyai.ts +16 -3
  201. package/ts/capabilities/hbcli.ts +46 -0
  202. package/ts/capabilities/session/benchmark.ts +4 -1
  203. package/ts/capabilities/session/extension-bridge.ts +377 -0
  204. package/ts/capabilities/session/extension-channel.ts +95 -0
  205. package/ts/capabilities/session/extension-distribution.ts +264 -0
  206. package/ts/capabilities/session/golden-score.ts +139 -0
  207. package/ts/capabilities/session/health-watch.ts +257 -0
  208. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  209. package/ts/capabilities/session/wizard.ts +140 -0
  210. package/ts/capabilities/session-login.ts +63 -2
  211. package/ts/capabilities/session-search.ts +105 -3
  212. package/ts/capabilities/weather.ts +141 -52
  213. package/ts/package.json +3 -3
  214. package/ts/src/artifact-gate.ts +365 -0
  215. package/ts/src/benchmark-agent-conformance.ts +448 -0
  216. package/ts/src/benchmark-environment-bridge.ts +348 -0
  217. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  218. package/ts/src/benchmark-tool-isolation.ts +166 -0
  219. package/ts/src/bookable-facts.ts +557 -0
  220. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  221. package/ts/src/booking-surface/canonical-schema.js +113 -0
  222. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  223. package/ts/src/booking-surface/contracts.ts +380 -0
  224. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  225. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  226. package/ts/src/booking-surface/error-codes.ts +101 -0
  227. package/ts/src/booking-surface/index.ts +12 -0
  228. package/ts/src/booking-surface/profile.ts +42 -0
  229. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  230. package/ts/src/booking-surface/runtime.ts +483 -0
  231. package/ts/src/booking-surface/server-v2.ts +247 -0
  232. package/ts/src/booking-surface/server.ts +324 -0
  233. package/ts/src/booking-surface/startup.ts +196 -0
  234. package/ts/src/booking-surface/validation-v2.ts +205 -0
  235. package/ts/src/booking-surface/validation.ts +453 -0
  236. package/ts/src/index.ts +161 -14
  237. package/ts/src/state-ledger.ts +1 -0
  238. package/ts/src/tool-budget.ts +165 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * Durable, task-scoped runtime for the embedded Booking Copilot.
3
+ *
4
+ * This is deliberately not a chat transcript. The ledger stores task identity,
5
+ * typed operation metadata, typed receipts and digests of prose/payloads. Raw
6
+ * user text and raw action criteria never enter durable state.
7
+ */
8
+
9
+ import { createHash, randomUUID } from 'node:crypto'
10
+ import type { StateLedger } from '../state-ledger.ts'
11
+ import {
12
+ type ActionReceiptContinuationV1,
13
+ type ActionReceiptV1,
14
+ type BookingExplanationEventV1,
15
+ type BookingQuestionEventV1,
16
+ type BookingReadActionKindV1,
17
+ type BookingReadActionV1,
18
+ type BookingStatusEventV1,
19
+ type BookingSurfaceEventV1,
20
+ type BookingTerminalEventV1,
21
+ type BookingErrorEventV1,
22
+ type BookingOperationEventV1,
23
+ type BookingSurfaceV1,
24
+ type UserTurnV1,
25
+ } from './contracts.ts'
26
+ import {
27
+ validateBookingCopilotTurnV1,
28
+ validateActionReceiptV1,
29
+ validateBookingReadActionV1,
30
+ validateBookingSurfaceEventV1,
31
+ } from './validation.ts'
32
+ import { normalizeBookingErrorCode, safeBookingErrorMessage } from './error-codes.ts'
33
+
34
+ export type {
35
+ ActionReceiptContinuationV1,
36
+ ActionReceiptV1,
37
+ BookingReadActionV1,
38
+ BookingSurfaceEventV1,
39
+ UserTurnV1,
40
+ } from './contracts.ts'
41
+
42
+ const LEDGER_SCHEMA = 'booking.copilot.ledger.v1' as const
43
+ const LEDGER_ACTOR = 'system:booking-copilot'
44
+ const TASK_STARTED = 'booking.copilot.task.started'
45
+ const USER_TURN_OBSERVED = 'booking.copilot.user.turn.observed'
46
+ const ACTION_ISSUED = 'booking.copilot.action.issued'
47
+ const RECEIPT_OBSERVED = 'booking.copilot.receipt.observed'
48
+ const EVENT_EMITTED = 'booking.copilot.event.emitted'
49
+
50
+ export type BookingCopilotTaskPhaseV1 = 'planning' | 'waiting_receipt'
51
+
52
+ export interface BookingActionCheckpointV1 {
53
+ actionId: string
54
+ kind: BookingReadActionKindV1
55
+ contextRef: string
56
+ expectedRevision: number
57
+ factRefs: string[]
58
+ reasonDigest: string
59
+ inputDigest: string
60
+ actionDigest: string
61
+ eventId: string
62
+ sequence: number
63
+ emittedAt: string
64
+ }
65
+
66
+ export interface BookingCopilotTaskStateV1 {
67
+ schemaVersion: 'booking.surface.v1'
68
+ taskId: string
69
+ contextRef: string
70
+ surface: BookingSurfaceV1
71
+ revision: number
72
+ allowedActions: BookingReadActionKindV1[]
73
+ userTurnCount: number
74
+ lastUserTurnDigest?: string
75
+ phase: BookingCopilotTaskPhaseV1
76
+ lastSequence: number
77
+ pendingAction?: BookingActionCheckpointV1
78
+ lastReceipt?: ActionReceiptV1
79
+ }
80
+
81
+ export type BookingSurfaceEventDraftV1 =
82
+ | Pick<BookingStatusEventV1, 'kind' | 'status'>
83
+ | Pick<BookingQuestionEventV1, 'kind' | 'question'>
84
+ | Pick<BookingExplanationEventV1, 'kind' | 'explanation'>
85
+ | Pick<BookingTerminalEventV1, 'kind' | 'terminal'>
86
+ | Pick<BookingErrorEventV1, 'kind' | 'error'>
87
+
88
+ export interface BookingCopilotRuntimeOptions {
89
+ idFactory?: (prefix: string) => string
90
+ now?: () => string
91
+ }
92
+
93
+ interface LedgerPayloadBase {
94
+ schema: typeof LEDGER_SCHEMA
95
+ taskId: string
96
+ contextRef: string
97
+ }
98
+
99
+ interface TaskStartedPayload extends LedgerPayloadBase {
100
+ surface: BookingSurfaceV1
101
+ revision: number
102
+ allowedActions: BookingReadActionKindV1[]
103
+ }
104
+
105
+ interface UserTurnObservedPayload extends LedgerPayloadBase {
106
+ requestDigest: string
107
+ }
108
+
109
+ interface ActionIssuedPayload extends LedgerPayloadBase {
110
+ action: BookingActionCheckpointV1
111
+ }
112
+
113
+ interface ReceiptObservedPayload extends LedgerPayloadBase {
114
+ receipt: ActionReceiptV1
115
+ receiptDigest: string
116
+ }
117
+
118
+ interface EventEmittedPayload extends LedgerPayloadBase {
119
+ eventId: string
120
+ sequence: number
121
+ emittedAt: string
122
+ eventKind: Exclude<BookingSurfaceEventV1['kind'], 'operation'>
123
+ contentDigest: string
124
+ }
125
+
126
+ interface BookingLedgerRow {
127
+ seq: number
128
+ kind: string
129
+ payload: string
130
+ }
131
+
132
+ function stableValue(value: unknown): unknown {
133
+ if (Array.isArray(value)) return value.map(stableValue)
134
+ if (typeof value !== 'object' || value === null) return value
135
+ const source = value as Record<string, unknown>
136
+ const target: Record<string, unknown> = {}
137
+ for (const key of Object.keys(source).sort()) target[key] = stableValue(source[key])
138
+ return target
139
+ }
140
+
141
+ function digest(value: unknown): string {
142
+ return createHash('sha256').update(JSON.stringify(stableValue(value))).digest('hex')
143
+ }
144
+
145
+ function checkpointDigest(action: Pick<BookingActionCheckpointV1, 'actionId' | 'kind' | 'contextRef' | 'expectedRevision' | 'factRefs' | 'reasonDigest' | 'inputDigest' | 'eventId' | 'sequence' | 'emittedAt'>): string {
146
+ return digest({
147
+ actionId: action.actionId,
148
+ kind: action.kind,
149
+ contextRef: action.contextRef,
150
+ expectedRevision: action.expectedRevision,
151
+ factRefs: action.factRefs,
152
+ reasonDigest: action.reasonDigest,
153
+ inputDigest: action.inputDigest,
154
+ eventId: action.eventId,
155
+ sequence: action.sequence,
156
+ emittedAt: action.emittedAt,
157
+ })
158
+ }
159
+
160
+ function actionMatchesCheckpoint(action: BookingReadActionV1, checkpoint: BookingActionCheckpointV1): boolean {
161
+ return checkpoint.actionDigest === checkpointDigest({
162
+ actionId: checkpoint.actionId,
163
+ kind: action.kind,
164
+ contextRef: action.contextRef,
165
+ expectedRevision: action.expectedRevision,
166
+ factRefs: action.factRefs,
167
+ reasonDigest: digest(action.reason),
168
+ inputDigest: digest(action.input),
169
+ eventId: checkpoint.eventId,
170
+ sequence: checkpoint.sequence,
171
+ emittedAt: checkpoint.emittedAt,
172
+ })
173
+ }
174
+
175
+ function assertSafeFactRefs(action: BookingReadActionV1): void {
176
+ const opaque = (ref: string) => /^[A-Za-z0-9][A-Za-z0-9:._-]*$/.test(ref) && !/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/i.test(ref)
177
+ if (!opaque(action.actionId)) throw new Error('unsafe_action_id')
178
+ if (action.factRefs.some((ref) => !opaque(ref))) throw new Error('unsafe_fact_ref')
179
+ }
180
+
181
+ function validationMessage(result: ReturnType<typeof validateBookingCopilotTurnV1>): string {
182
+ return result.ok ? '' : result.errors.join('; ')
183
+ }
184
+
185
+ function assertTaskId(taskId: string): void {
186
+ if (!taskId) throw new Error('invalid_task_id')
187
+ }
188
+
189
+ function sameActionAllowlist(left: readonly BookingReadActionKindV1[], right: readonly BookingReadActionKindV1[]): boolean {
190
+ return left.length === right.length && [...left].sort().every((kind, index) => kind === [...right].sort()[index])
191
+ }
192
+
193
+ export class BookingCopilotTaskRuntime {
194
+ private readonly idFactory: (prefix: string) => string
195
+ private readonly now: () => string
196
+
197
+ constructor(
198
+ private readonly ledger: StateLedger,
199
+ options: BookingCopilotRuntimeOptions = {},
200
+ ) {
201
+ this.idFactory = options.idFactory ?? ((prefix) => `${prefix}-${randomUUID()}`)
202
+ this.now = options.now ?? (() => new Date().toISOString())
203
+ }
204
+
205
+ startTask(turn: UserTurnV1): BookingCopilotTaskStateV1 {
206
+ const validation = validateBookingCopilotTurnV1(turn)
207
+ if (!validation.ok || turn.kind !== 'user.turn') throw new Error(`invalid_planner_turn:${validationMessage(validation)}`)
208
+ const taskId = turn.taskId ?? this.idFactory('task')
209
+ assertTaskId(taskId)
210
+ const requestDigest = digest(turn.request.text)
211
+ // The task fold and its user-turn append must share one write transaction.
212
+ // An immediate transaction serializes separate processes before either can
213
+ // derive state (and therefore an ordinal/next event sequence) from a stale
214
+ // snapshot.
215
+ const apply = this.ledger.db.transaction(() => {
216
+ const existing = this.resumeTask(taskId)
217
+ if (existing) {
218
+ if (existing.contextRef !== turn.workspace.contextRef) throw new Error('task_conflict:context_mismatch')
219
+ if (existing.surface !== turn.workspace.surface) throw new Error('task_conflict:surface_mismatch')
220
+ if (existing.revision !== turn.workspace.revision) throw new Error('task_conflict:revision_mismatch')
221
+ if (!sameActionAllowlist(existing.allowedActions, turn.workspace.capabilities.allowedActions)) {
222
+ throw new Error('task_conflict:capability_mismatch')
223
+ }
224
+ if (existing.phase === 'waiting_receipt') throw new Error('receipt_required')
225
+ this.observeUserTurn(taskId, existing.contextRef, requestDigest, existing.userTurnCount + 1)
226
+ return this.requireTask(taskId)
227
+ }
228
+ const foreign = this.ledger.db.prepare(
229
+ "SELECT 1 AS present FROM events WHERE tenant_id = ? AND run_id = ? AND kind LIKE 'booking.copilot.v2.%' LIMIT 1",
230
+ ).get(this.ledger.tenant, taskId) as { present: number } | undefined
231
+ if (foreign) throw new Error('task_conflict:protocol_version')
232
+ const payload: TaskStartedPayload = {
233
+ schema: LEDGER_SCHEMA,
234
+ taskId,
235
+ contextRef: turn.workspace.contextRef,
236
+ surface: turn.workspace.surface,
237
+ revision: turn.workspace.revision,
238
+ allowedActions: [...turn.workspace.capabilities.allowedActions],
239
+ }
240
+ this.appendLedgerEvent(TASK_STARTED, taskId, payload, `booking-copilot:task:${taskId}`)
241
+ this.observeUserTurn(taskId, turn.workspace.contextRef, requestDigest, 1)
242
+ return this.requireTask(taskId)
243
+ })
244
+ return apply.immediate()
245
+ }
246
+
247
+ resumeTask(taskId: string): BookingCopilotTaskStateV1 | null {
248
+ assertTaskId(taskId)
249
+ const rows = this.readTaskRows(taskId)
250
+ if (rows.length === 0) return null
251
+ let state: BookingCopilotTaskStateV1 | null = null
252
+ for (const row of rows) {
253
+ const payload = JSON.parse(row.payload) as Record<string, unknown>
254
+ if (payload.schema !== LEDGER_SCHEMA || payload.taskId !== taskId) throw new Error(`ledger_corrupt:${taskId}:seq=${row.seq}`)
255
+ if (row.kind === TASK_STARTED) {
256
+ const started = payload as unknown as TaskStartedPayload
257
+ if (state) throw new Error(`ledger_corrupt:${taskId}:duplicate_start`)
258
+ state = {
259
+ schemaVersion: 'booking.surface.v1',
260
+ taskId,
261
+ contextRef: started.contextRef,
262
+ surface: started.surface,
263
+ revision: started.revision,
264
+ allowedActions: [...started.allowedActions],
265
+ userTurnCount: 0,
266
+ phase: 'planning',
267
+ lastSequence: 0,
268
+ }
269
+ continue
270
+ }
271
+ if (!state) throw new Error(`ledger_corrupt:${taskId}:event_before_start`)
272
+ if (payload.contextRef !== state.contextRef) throw new Error(`ledger_corrupt:${taskId}:context_drift`)
273
+ if (row.kind === USER_TURN_OBSERVED) {
274
+ const observed = payload as unknown as UserTurnObservedPayload
275
+ state.userTurnCount += 1
276
+ state.lastUserTurnDigest = observed.requestDigest
277
+ } else if (row.kind === ACTION_ISSUED) {
278
+ const issued = payload as unknown as ActionIssuedPayload
279
+ if (issued.action.actionDigest !== checkpointDigest(issued.action)) throw new Error(`ledger_corrupt:${taskId}:action`)
280
+ if (state.pendingAction) throw new Error(`ledger_corrupt:${taskId}:parallel_actions`)
281
+ state.pendingAction = issued.action
282
+ state.phase = 'waiting_receipt'
283
+ state.lastSequence = Math.max(state.lastSequence, issued.action.sequence)
284
+ } else if (row.kind === RECEIPT_OBSERVED) {
285
+ const observed = payload as unknown as ReceiptObservedPayload
286
+ if (observed.receiptDigest !== digest(observed.receipt) || !validateActionReceiptV1(observed.receipt).ok) {
287
+ throw new Error(`ledger_corrupt:${taskId}:receipt`)
288
+ }
289
+ if (!state.pendingAction || state.pendingAction.actionId !== observed.receipt.actionId) {
290
+ throw new Error(`ledger_corrupt:${taskId}:orphan_receipt`)
291
+ }
292
+ state.lastReceipt = observed.receipt
293
+ state.revision = observed.receipt.revision
294
+ delete state.pendingAction
295
+ state.phase = 'planning'
296
+ } else if (row.kind === EVENT_EMITTED) {
297
+ const emitted = payload as unknown as EventEmittedPayload
298
+ state.lastSequence = Math.max(state.lastSequence, emitted.sequence)
299
+ }
300
+ }
301
+ return state
302
+ }
303
+
304
+ issueOperation(taskId: string, action: BookingReadActionV1): BookingOperationEventV1 {
305
+ const validation = validateBookingReadActionV1(action)
306
+ if (!validation.ok) throw new Error(`invalid_action:${validation.errors.join('; ')}`)
307
+ assertSafeFactRefs(action)
308
+ const apply = this.ledger.db.transaction(() => {
309
+ const state = this.requireTask(taskId)
310
+ const reasonDigest = digest(action.reason)
311
+ const inputDigest = digest(action.input)
312
+ if (state.pendingAction) {
313
+ if (state.pendingAction.actionId !== action.actionId) throw new Error('receipt_required')
314
+ if (!actionMatchesCheckpoint(action, state.pendingAction)) throw new Error('action_conflict')
315
+ return {
316
+ schemaVersion: 'booking.surface.v1' as const, eventId: state.pendingAction.eventId, taskId,
317
+ contextRef: state.pendingAction.contextRef, sequence: state.pendingAction.sequence,
318
+ emittedAt: state.pendingAction.emittedAt, kind: 'operation' as const, action,
319
+ }
320
+ }
321
+ const historicalReceipt = this.ledger.db.prepare(
322
+ `SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ?`,
323
+ ).all(this.ledger.tenant, taskId, RECEIPT_OBSERVED) as Array<{ payload: string }>
324
+ if (historicalReceipt.some((row) => (JSON.parse(row.payload) as ReceiptObservedPayload).receipt?.actionId === action.actionId)) throw new Error('action_already_receipted')
325
+ if (state.lastReceipt?.actionId === action.actionId) throw new Error('action_already_receipted')
326
+ if (action.contextRef !== state.contextRef) throw new Error('context_mismatch')
327
+ if (!new Set<string>(state.allowedActions).has(action.kind)) throw new Error('unsupported_action')
328
+ if (action.expectedRevision !== state.revision) throw new Error('stale_revision')
329
+ const event: BookingOperationEventV1 = {
330
+ schemaVersion: 'booking.surface.v1', eventId: this.idFactory('event'), taskId,
331
+ contextRef: state.contextRef, sequence: state.lastSequence + 1, emittedAt: this.now(), kind: 'operation', action,
332
+ }
333
+ const eventValidation = validateBookingSurfaceEventV1(event)
334
+ if (!eventValidation.ok) throw new Error(`invalid_operation_event:${eventValidation.errors.join('; ')}`)
335
+ const checkpointBase = { actionId: action.actionId, kind: action.kind, contextRef: action.contextRef, expectedRevision: action.expectedRevision, factRefs: [...action.factRefs], reasonDigest, inputDigest, eventId: event.eventId, sequence: event.sequence, emittedAt: event.emittedAt }
336
+ const checkpoint: BookingActionCheckpointV1 = { ...checkpointBase, actionDigest: checkpointDigest(checkpointBase) }
337
+ const payload: ActionIssuedPayload = { schema: LEDGER_SCHEMA, taskId, contextRef: state.contextRef, action: checkpoint }
338
+ this.appendActionIssued(taskId, payload, `booking-copilot:action:${taskId.length}:${taskId}:${action.actionId.length}:${action.actionId}`)
339
+ return event
340
+ })
341
+ return apply.immediate()
342
+ }
343
+
344
+ continueWithReceipt(turn: ActionReceiptContinuationV1): BookingCopilotTaskStateV1 {
345
+ const validation = validateBookingCopilotTurnV1(turn)
346
+ if (!validation.ok || turn.kind !== 'action.receipt.continuation') {
347
+ throw new Error(`invalid_receipt_continuation:${validationMessage(validation)}`)
348
+ }
349
+ const receiptDigest = digest(turn.receipt)
350
+ const apply = this.ledger.db.transaction(() => {
351
+ const state = this.requireTask(turn.taskId)
352
+ if (turn.workspace.contextRef !== state.contextRef || turn.receipt.contextRef !== state.contextRef) throw new Error('context_mismatch')
353
+ // Receipt replay is keyed by task + action, not merely by the latest fold state.
354
+ const history = this.ledger.db.prepare(
355
+ `SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = ? ORDER BY seq`,
356
+ ).all(this.ledger.tenant, turn.taskId, RECEIPT_OBSERVED) as Array<{ payload: string }>
357
+ for (const row of history) {
358
+ const prior = JSON.parse(row.payload) as ReceiptObservedPayload
359
+ if (prior.receipt?.actionId !== turn.receipt.actionId) continue
360
+ if (prior.schema !== LEDGER_SCHEMA || prior.taskId !== turn.taskId || prior.contextRef !== state.contextRef || prior.receiptDigest !== digest(prior.receipt) || !validateActionReceiptV1(prior.receipt).ok) throw new Error('ledger_corrupt:receipt')
361
+ if (prior.receiptDigest !== receiptDigest) throw new Error('receipt_conflict')
362
+ return state
363
+ }
364
+ if (!state.pendingAction) throw new Error('unexpected_receipt')
365
+ if (state.pendingAction.actionId !== turn.receipt.actionId) throw new Error('receipt_action_mismatch')
366
+ if (turn.receipt.revision < state.pendingAction.expectedRevision) throw new Error('revision_regression')
367
+ const payload: ReceiptObservedPayload = { schema: LEDGER_SCHEMA, taskId: turn.taskId, contextRef: state.contextRef, receipt: turn.receipt, receiptDigest }
368
+ const idemKey = `booking-copilot:receipt:${turn.taskId.length}:${turn.taskId}:${turn.receipt.actionId.length}:${turn.receipt.actionId}`
369
+ const info = this.ledger.db.prepare(
370
+ `INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
371
+ ).run(this.ledger.tenant, this.now(), LEDGER_ACTOR, RECEIPT_OBSERVED, turn.taskId, JSON.stringify(payload), idemKey, turn.taskId)
372
+ if (info.changes !== 1) throw new Error('ledger_write_failed:receipt')
373
+ return this.requireTask(turn.taskId)
374
+ })
375
+ return apply.immediate()
376
+ }
377
+
378
+ emitEvent(taskId: string, draft: BookingSurfaceEventDraftV1): Exclude<BookingSurfaceEventV1, BookingOperationEventV1> {
379
+ // Sequence allocation is part of the same immediate transaction as the
380
+ // durable append. This makes the fold read and next-sequence write one
381
+ // critical section across independent runtime processes.
382
+ const apply = this.ledger.db.transaction(() => {
383
+ const state = this.requireTask(taskId)
384
+ const safeDraft = draft.kind === 'error'
385
+ ? { ...draft, error: { ...draft.error, code: normalizeBookingErrorCode(draft.error.code), message: safeBookingErrorMessage(normalizeBookingErrorCode(draft.error.code)) } }
386
+ : draft
387
+ const event = {
388
+ schemaVersion: 'booking.surface.v1',
389
+ eventId: this.idFactory('event'),
390
+ taskId,
391
+ contextRef: state.contextRef,
392
+ sequence: state.lastSequence + 1,
393
+ emittedAt: this.now(),
394
+ ...safeDraft,
395
+ } as Exclude<BookingSurfaceEventV1, BookingOperationEventV1>
396
+ const validation = validateBookingSurfaceEventV1(event)
397
+ if (!validation.ok) throw new Error(`invalid_event:${validation.errors.join('; ')}`)
398
+ const payload: EventEmittedPayload = {
399
+ schema: LEDGER_SCHEMA,
400
+ taskId,
401
+ contextRef: state.contextRef,
402
+ eventId: event.eventId,
403
+ sequence: event.sequence,
404
+ emittedAt: event.emittedAt,
405
+ eventKind: event.kind,
406
+ contentDigest: digest(safeDraft),
407
+ }
408
+ this.appendLedgerEvent(EVENT_EMITTED, taskId, payload, `booking-copilot:event:${taskId}:${event.eventId}`)
409
+ return event
410
+ })
411
+ return apply.immediate()
412
+ }
413
+
414
+ private requireTask(taskId: string): BookingCopilotTaskStateV1 {
415
+ const state = this.resumeTask(taskId)
416
+ if (!state) throw new Error('task_not_found')
417
+ return state
418
+ }
419
+
420
+ private readTaskRows(taskId: string): BookingLedgerRow[] {
421
+ return this.ledger.db.prepare(
422
+ `SELECT seq, kind, payload FROM events
423
+ WHERE tenant_id = ? AND run_id = ?
424
+ AND kind IN (?, ?, ?, ?, ?)
425
+ ORDER BY seq`,
426
+ ).all(
427
+ this.ledger.tenant,
428
+ taskId,
429
+ TASK_STARTED,
430
+ USER_TURN_OBSERVED,
431
+ ACTION_ISSUED,
432
+ RECEIPT_OBSERVED,
433
+ EVENT_EMITTED,
434
+ ) as BookingLedgerRow[]
435
+ }
436
+
437
+ private observeUserTurn(taskId: string, contextRef: string, requestDigest: string, ordinal: number): void {
438
+ const payload: UserTurnObservedPayload = {
439
+ schema: LEDGER_SCHEMA,
440
+ taskId,
441
+ contextRef,
442
+ requestDigest,
443
+ }
444
+ this.appendLedgerEvent(
445
+ USER_TURN_OBSERVED,
446
+ taskId,
447
+ payload,
448
+ `booking-copilot:user-turn:${taskId}:${ordinal}`,
449
+ )
450
+ }
451
+
452
+ private appendLedgerEvent(kind: string, taskId: string, payload: LedgerPayloadBase, idemKey: string): number {
453
+ const result = this.ledger.db.prepare(
454
+ `INSERT OR IGNORE INTO events
455
+ (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
456
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
457
+ ).run(
458
+ this.ledger.tenant,
459
+ this.now(),
460
+ LEDGER_ACTOR,
461
+ kind,
462
+ taskId,
463
+ JSON.stringify(payload),
464
+ idemKey,
465
+ taskId,
466
+ )
467
+ if (result.changes > 0) return Number(result.lastInsertRowid)
468
+ const existing = this.ledger.db.prepare(
469
+ 'SELECT seq FROM events WHERE tenant_id = ? AND idem_key = ?',
470
+ ).get(this.ledger.tenant, idemKey) as { seq: number } | undefined
471
+ if (!existing) throw new Error(`ledger_write_failed:${kind}`)
472
+ return existing.seq
473
+ }
474
+
475
+ private appendActionIssued(taskId: string, payload: ActionIssuedPayload, idemKey: string): number {
476
+ const result = this.ledger.db.prepare(
477
+ `INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id)
478
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
479
+ ).run(this.ledger.tenant, this.now(), LEDGER_ACTOR, ACTION_ISSUED, taskId, JSON.stringify(payload), idemKey, taskId)
480
+ if (result.changes !== 1) throw new Error('ledger_write_failed:action')
481
+ return Number(result.lastInsertRowid)
482
+ }
483
+ }