@danceiny/gotry 0.0.1-rc.16 → 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 (229) hide show
  1. package/README.md +47 -13
  2. package/README.zh-CN.md +20 -10
  3. package/bin/gotry-booking-copilot.js +53 -0
  4. package/bin/gotry-bootstrap.js +111 -51
  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 +5 -1
  10. package/dist/capabilities/agent-reach-deep.js +1 -1
  11. package/dist/capabilities/agent-reach.js +1 -1
  12. package/dist/capabilities/anything.js +1 -1
  13. package/dist/capabilities/artifacts.js +1 -1
  14. package/dist/capabilities/effect.js +1 -1
  15. package/dist/capabilities/fact-log.js +1 -1
  16. package/dist/capabilities/flyai.js +20 -6
  17. package/dist/capabilities/hbcli.js +1 -1
  18. package/dist/capabilities/incident-log.js +1 -1
  19. package/dist/capabilities/model-override.js +18 -0
  20. package/dist/capabilities/opensky.js +1 -1
  21. package/dist/capabilities/resilience.js +1 -1
  22. package/dist/capabilities/session/action-cache.js +1 -1
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
  24. package/dist/capabilities/session/adapters/meituan-local.js +1 -1
  25. package/dist/capabilities/session/benchmark.js +1 -1
  26. package/dist/capabilities/session/extension-bridge.js +19 -6
  27. package/dist/capabilities/session/extension-channel.js +3 -3
  28. package/dist/capabilities/session/extension-distribution.js +234 -0
  29. package/dist/capabilities/session/extract.js +1 -1
  30. package/dist/capabilities/session/golden-score.js +92 -0
  31. package/dist/capabilities/session/health-watch.js +1 -1
  32. package/dist/capabilities/session/read-guard.js +1 -1
  33. package/dist/capabilities/session/static-flight-golden.js +137 -0
  34. package/dist/capabilities/session/transport.js +1 -1
  35. package/dist/capabilities/session/wizard.js +21 -251
  36. package/dist/capabilities/session-consent.js +1 -1
  37. package/dist/capabilities/session-login.js +11 -5
  38. package/dist/capabilities/session-search.js +46 -4
  39. package/dist/capabilities/weather.js +168 -46
  40. package/dist/data/session-golden-20.json +25 -0
  41. package/dist/data/sf-golden-manifest.json +102 -0
  42. package/dist/data/sf-static-routes.json +91 -0
  43. package/dist/scripts/action-cache-tests.js +1 -1
  44. package/dist/scripts/agent-planning-budget-e2e.js +227 -0
  45. package/dist/scripts/agent-planning-budget-tests.js +173 -0
  46. package/dist/scripts/agent-reach-deep-tests.js +1 -1
  47. package/dist/scripts/agent-reach-tests.js +1 -1
  48. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  49. package/dist/scripts/anything-tests.js +1 -1
  50. package/dist/scripts/async-collect.js +1 -1
  51. package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
  52. package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
  53. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
  54. package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
  55. package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
  56. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
  57. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
  58. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
  59. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
  60. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
  61. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
  62. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
  63. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
  64. package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
  65. package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
  66. package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
  67. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
  68. package/dist/scripts/booking-saga-tests.js +1 -1
  69. package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
  70. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
  71. package/dist/scripts/bootstrap-tests.js +30 -10
  72. package/dist/scripts/build-changelog.js +1 -1
  73. package/dist/scripts/changelog-tests.js +1 -1
  74. package/dist/scripts/companion-tests.js +1 -1
  75. package/dist/scripts/diff-test.js +1 -1
  76. package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
  77. package/dist/scripts/dsh-runtime-closure.js +150 -0
  78. package/dist/scripts/effect-tests.js +1 -1
  79. package/dist/scripts/engine-run.js +1 -1
  80. package/dist/scripts/engine-tests.js +1 -1
  81. package/dist/scripts/evaluation-cadence-tests.js +347 -0
  82. package/dist/scripts/evaluation-contract-tests.js +574 -0
  83. package/dist/scripts/extension-distribution-cli.js +35 -0
  84. package/dist/scripts/extension-distribution-tests.js +341 -0
  85. package/dist/scripts/extension-tests.js +128 -23
  86. package/dist/scripts/fact-gate-tests.js +1 -1
  87. package/dist/scripts/flyai-tests.js +1 -1
  88. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  89. package/dist/scripts/hbcli-tests.js +1 -1
  90. package/dist/scripts/health-watch-cli.js +1 -1
  91. package/dist/scripts/i18n-tests.js +1 -1
  92. package/dist/scripts/incident-tests.js +1 -1
  93. package/dist/scripts/journey-tests.js +1 -1
  94. package/dist/scripts/ledger-tests.js +1 -1
  95. package/dist/scripts/ledger-workflow-crash.js +1 -1
  96. package/dist/scripts/memory-capture-tests.js +1 -1
  97. package/dist/scripts/memory-decay-tests.js +1 -1
  98. package/dist/scripts/memory-metrics.js +1 -1
  99. package/dist/scripts/memory-value-report.js +1 -1
  100. package/dist/scripts/model-override-e2e.js +176 -0
  101. package/dist/scripts/nightly-evidence-tests.js +1 -1
  102. package/dist/scripts/nightly-evidence.js +1 -1
  103. package/dist/scripts/nudge-digest.js +1 -1
  104. package/dist/scripts/onboarding-tests.js +21 -53
  105. package/dist/scripts/opensky-check.js +1 -1
  106. package/dist/scripts/opensky-tests.js +1 -1
  107. package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
  108. package/dist/scripts/price-drift-tests.js +1 -1
  109. package/dist/scripts/price-drift-watch.js +1 -1
  110. package/dist/scripts/probe-poi-tests.js +1 -1
  111. package/dist/scripts/product-metrics.js +1 -1
  112. package/dist/scripts/publish-preverify.js +40 -4
  113. package/dist/scripts/realtime-pricing-tests.js +1 -1
  114. package/dist/scripts/replay-async.js +1 -1
  115. package/dist/scripts/replay-real.js +1 -1
  116. package/dist/scripts/replay.js +1 -1
  117. package/dist/scripts/session-attach-diagnose.js +1 -1
  118. package/dist/scripts/session-attach-poc.js +1 -1
  119. package/dist/scripts/session-benchmark.js +1 -1
  120. package/dist/scripts/session-extract-tests.js +1 -1
  121. package/dist/scripts/session-login.js +1 -1
  122. package/dist/scripts/session-tests.js +70 -20
  123. package/dist/scripts/sf-live-benchmark.js +338 -0
  124. package/dist/scripts/sf-live-cli-tests.js +21 -0
  125. package/dist/scripts/sf-soft-score-tests.js +108 -0
  126. package/dist/scripts/sf-summary.js +93 -0
  127. package/dist/scripts/skeleton-check.js +1 -1
  128. package/dist/scripts/skeleton-integration-test.js +1 -1
  129. package/dist/scripts/skills-contract-tests.js +1 -1
  130. package/dist/scripts/smoke-session-gate-tests.js +29 -0
  131. package/dist/scripts/smoke.js +79 -33
  132. package/dist/scripts/state-cli-tests.js +1 -1
  133. package/dist/scripts/state-cli.js +1 -1
  134. package/dist/scripts/static-golden-tests.js +299 -0
  135. package/dist/scripts/time-eval-tests.js +1 -1
  136. package/dist/scripts/travel-timeline-tests.js +1 -1
  137. package/dist/scripts/unified-tests.js +1 -1
  138. package/dist/scripts/weather-tests.js +694 -44
  139. package/dist/scripts/z3-race-tests.js +1 -1
  140. package/dist/src/artifact-gate.js +1 -1
  141. package/dist/src/benchmark-agent-conformance.js +370 -0
  142. package/dist/src/benchmark-environment-bridge.js +384 -0
  143. package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
  144. package/dist/src/benchmark-tool-isolation.js +124 -0
  145. package/dist/src/bookable-facts.js +1 -1
  146. package/dist/src/booking-saga.js +1 -1
  147. package/dist/src/booking-surface/availability-policy-v2.js +830 -0
  148. package/dist/src/booking-surface/canonical-schema.js +113 -0
  149. package/dist/src/booking-surface/contracts-v2.js +89 -0
  150. package/dist/src/booking-surface/contracts.js +46 -0
  151. package/dist/src/booking-surface/dsh-planner.js +453 -0
  152. package/dist/src/booking-surface/dsh-plugin.js +93 -0
  153. package/dist/src/booking-surface/error-codes.js +94 -0
  154. package/dist/src/booking-surface/index.js +15 -0
  155. package/dist/src/booking-surface/profile.js +68 -0
  156. package/dist/src/booking-surface/runtime-v2.js +1771 -0
  157. package/dist/src/booking-surface/runtime.js +351 -0
  158. package/dist/src/booking-surface/server-v2.js +334 -0
  159. package/dist/src/booking-surface/server.js +302 -0
  160. package/dist/src/booking-surface/startup.js +159 -0
  161. package/dist/src/booking-surface/validation-v2.js +319 -0
  162. package/dist/src/booking-surface/validation.js +809 -0
  163. package/dist/src/bridge.js +1 -1
  164. package/dist/src/companions.js +1 -1
  165. package/dist/src/contracts.js +1 -1
  166. package/dist/src/dsh-llm.js +1 -1
  167. package/dist/src/engine.js +1 -1
  168. package/dist/src/evaluation-cadence.js +234 -0
  169. package/dist/src/evaluation-contracts.js +906 -0
  170. package/dist/src/i18n.js +1 -1
  171. package/dist/src/index.js +50 -19
  172. package/dist/src/journey.js +1 -1
  173. package/dist/src/loop.js +1 -1
  174. package/dist/src/memory-capture.js +1 -1
  175. package/dist/src/memory-decay.js +1 -1
  176. package/dist/src/memory-utility.js +1 -1
  177. package/dist/src/mock-llm.js +1 -1
  178. package/dist/src/model.js +1 -1
  179. package/dist/src/realtime-pricing.js +1 -1
  180. package/dist/src/slot-spec.js +1 -1
  181. package/dist/src/state-ledger.js +2 -1
  182. package/dist/src/time-anchor.js +1 -1
  183. package/dist/src/tool-budget.js +136 -0
  184. package/dist/src/tool-packet.js +1 -1
  185. package/dist/src/travel-slots.js +1 -1
  186. package/dist/src/travel-timeline.js +1 -1
  187. package/dist/src/unified.js +1 -1
  188. package/dist/src/wish-pool.js +1 -1
  189. package/dist/src/z3-shared.js +1 -1
  190. package/extension/README.md +31 -7
  191. package/package.json +286 -11
  192. package/schemas/booking.surface.v1.schema.json +927 -0
  193. package/schemas/booking.surface.v2.schema.json +61 -0
  194. package/ts/capabilities/flyai.ts +16 -3
  195. package/ts/capabilities/session/extension-bridge.ts +37 -14
  196. package/ts/capabilities/session/extension-channel.ts +6 -3
  197. package/ts/capabilities/session/extension-distribution.ts +264 -0
  198. package/ts/capabilities/session/golden-score.ts +139 -0
  199. package/ts/capabilities/session/health-watch.ts +1 -1
  200. package/ts/capabilities/session/static-flight-golden.ts +209 -0
  201. package/ts/capabilities/session/wizard.ts +34 -176
  202. package/ts/capabilities/session-login.ts +15 -5
  203. package/ts/capabilities/session-search.ts +40 -3
  204. package/ts/capabilities/weather.ts +141 -52
  205. package/ts/package.json +3 -3
  206. package/ts/src/benchmark-agent-conformance.ts +448 -0
  207. package/ts/src/benchmark-environment-bridge.ts +348 -0
  208. package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
  209. package/ts/src/benchmark-tool-isolation.ts +166 -0
  210. package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
  211. package/ts/src/booking-surface/canonical-schema.js +113 -0
  212. package/ts/src/booking-surface/contracts-v2.ts +118 -0
  213. package/ts/src/booking-surface/contracts.ts +380 -0
  214. package/ts/src/booking-surface/dsh-planner.ts +452 -0
  215. package/ts/src/booking-surface/dsh-plugin.js +93 -0
  216. package/ts/src/booking-surface/error-codes.ts +101 -0
  217. package/ts/src/booking-surface/index.ts +12 -0
  218. package/ts/src/booking-surface/profile.ts +42 -0
  219. package/ts/src/booking-surface/runtime-v2.ts +1466 -0
  220. package/ts/src/booking-surface/runtime.ts +483 -0
  221. package/ts/src/booking-surface/server-v2.ts +247 -0
  222. package/ts/src/booking-surface/server.ts +324 -0
  223. package/ts/src/booking-surface/startup.ts +196 -0
  224. package/ts/src/booking-surface/validation-v2.ts +205 -0
  225. package/ts/src/booking-surface/validation.ts +453 -0
  226. package/ts/src/index.ts +64 -11
  227. package/ts/src/state-ledger.ts +1 -0
  228. package/ts/src/tool-budget.ts +165 -0
  229. package/dist/scripts/wizard-bootstrap.js +0 -32
@@ -0,0 +1,447 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { ensureLedger } from '../src/state-ledger.js';
8
+ import { BookingCopilotTaskRuntime } from '../src/booking-surface/runtime.js';
9
+ import { validateActionReceiptV1 } from '../src/booking-surface/validation.js';
10
+ const childIndex = process.argv.indexOf('--child');
11
+ const child = childIndex >= 0;
12
+ const root = child ? process.argv[childIndex + 1] : undefined;
13
+ const actionId = child ? process.argv[childIndex + 2] : undefined;
14
+ const variant = child ? process.argv[childIndex + 3] : undefined;
15
+ const release = root ? join(root, 'release') : '';
16
+ const workspace = {
17
+ schemaVersion: 'booking.surface.v1',
18
+ contextRef: 'ctx-concurrency-opaque',
19
+ surface: 'tenant',
20
+ revision: 0,
21
+ locale: 'en-US',
22
+ currency: 'AED',
23
+ searchDraft: {},
24
+ results: {
25
+ status: 'idle'
26
+ },
27
+ visibleHotels: [],
28
+ loadedOffers: [],
29
+ shortlistedOfferRefs: [],
30
+ capabilities: {
31
+ surface: 'tenant',
32
+ allowedActions: [
33
+ 'search.run'
34
+ ]
35
+ }
36
+ };
37
+ const action = {
38
+ schemaVersion: 'booking.surface.v1',
39
+ kind: 'search.run',
40
+ actionId: 'action-concurrency-opaque',
41
+ contextRef: workspace.contextRef,
42
+ expectedRevision: 0,
43
+ reason: 'opaque reason',
44
+ factRefs: [
45
+ 'fact-opaque'
46
+ ],
47
+ input: {}
48
+ };
49
+ function receipt(which) {
50
+ const value = {
51
+ schemaVersion: 'booking.surface.v1',
52
+ kind: 'action.receipt',
53
+ actionId: action.actionId,
54
+ contextRef: workspace.contextRef,
55
+ status: 'applied',
56
+ revision: 1,
57
+ observation: {
58
+ kind: 'search.state',
59
+ searchSessionRef: `session-${which === 'same-a' || which === 'same-b' ? 'same' : which}`,
60
+ resultCount: 1
61
+ },
62
+ resultContract: {
63
+ outcome: 'complete',
64
+ hardCriteriaMet: true,
65
+ factRefs: [
66
+ `fact-${which === 'same-a' || which === 'same-b' ? 'same' : which}`
67
+ ],
68
+ gapCodes: []
69
+ },
70
+ undoToken: `undo-${which === 'same-a' || which === 'same-b' ? 'same' : which}`
71
+ };
72
+ if (which === 'same-b') return {
73
+ undoToken: value.undoToken,
74
+ resultContract: value.resultContract,
75
+ observation: value.observation,
76
+ revision: value.revision,
77
+ status: value.status,
78
+ contextRef: value.contextRef,
79
+ actionId: value.actionId,
80
+ kind: value.kind,
81
+ schemaVersion: value.schemaVersion
82
+ };
83
+ return value;
84
+ }
85
+ function continuation(r, taskId = 'task-concurrency-opaque') {
86
+ return {
87
+ schemaVersion: 'booking.surface.v1',
88
+ kind: 'action.receipt.continuation',
89
+ taskId,
90
+ workspace: {
91
+ ...workspace,
92
+ revision: 1
93
+ },
94
+ receipt: r
95
+ };
96
+ }
97
+ if (process.argv.includes('--replay')) {
98
+ const replayRoot = process.argv[process.argv.indexOf('--replay') + 1];
99
+ const replayVariant = process.argv[process.argv.indexOf('--replay') + 2];
100
+ const replayLedger = ensureLedger(replayRoot);
101
+ try {
102
+ new BookingCopilotTaskRuntime(replayLedger).continueWithReceipt(continuation(receipt(replayVariant)));
103
+ console.log(JSON.stringify({
104
+ result: 'success'
105
+ }));
106
+ } catch (error) {
107
+ console.log(JSON.stringify({
108
+ result: 'error',
109
+ error: error instanceof Error ? error.message : String(error)
110
+ }));
111
+ }
112
+ replayLedger.close();
113
+ process.exit(0);
114
+ }
115
+ if (child) {
116
+ const ledger = ensureLedger(root);
117
+ const r = new BookingCopilotTaskRuntime(ledger);
118
+ writeFileSync(join(root, `started-${variant}`), 'started');
119
+ const startDeadline = Date.now() + 10_000;
120
+ while(!existsSync(join(root, 'start'))){
121
+ if (Date.now() > startDeadline) throw new Error('child_start_timeout');
122
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
123
+ }
124
+ const originalResume = r.resumeTask.bind(r);
125
+ let firstPendingRead = true;
126
+ r.resumeTask = (taskId)=>{
127
+ const state = originalResume(taskId);
128
+ if (firstPendingRead && state?.pendingAction) {
129
+ firstPendingRead = false;
130
+ writeFileSync(join(root, `ready-${variant}`), 'ready');
131
+ const deadline = Date.now() + 10_000;
132
+ while(!existsSync(release)){
133
+ if (Date.now() > deadline) throw new Error('child_timeout');
134
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
135
+ }
136
+ }
137
+ return state;
138
+ };
139
+ try {
140
+ r.continueWithReceipt(continuation(receipt(variant)));
141
+ console.log(JSON.stringify({
142
+ result: 'success',
143
+ variant
144
+ }));
145
+ } catch (error) {
146
+ console.log(JSON.stringify({
147
+ result: 'error',
148
+ variant,
149
+ error: error instanceof Error ? error.message : String(error)
150
+ }));
151
+ }
152
+ ledger.close();
153
+ process.exit(0);
154
+ }
155
+ function waitFor(predicate, timeoutMs) {
156
+ return new Promise((resolve, reject)=>{
157
+ const deadline = Date.now() + timeoutMs;
158
+ const tick = ()=>predicate() ? resolve() : Date.now() > deadline ? reject(new Error('proof_timeout')) : setTimeout(tick, 10);
159
+ tick();
160
+ });
161
+ }
162
+ async function runPair(stateRoot, left, right) {
163
+ const script = fileURLToPath(import.meta.url);
164
+ const args = [
165
+ script,
166
+ '--child',
167
+ stateRoot,
168
+ action.actionId
169
+ ];
170
+ const tsxBin = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url));
171
+ const children = [
172
+ left,
173
+ right
174
+ ].map((which)=>spawn(tsxBin, [
175
+ ...args,
176
+ which
177
+ ], {
178
+ cwd: process.cwd(),
179
+ stdio: [
180
+ 'ignore',
181
+ 'pipe',
182
+ 'pipe'
183
+ ]
184
+ }));
185
+ const outputs = children.map(()=>'');
186
+ children.forEach((p, i)=>{
187
+ p.stdout.on('data', (b)=>{
188
+ outputs[i] += b.toString();
189
+ });
190
+ p.stderr.on('data', (b)=>{
191
+ outputs[i] += b.toString();
192
+ });
193
+ p.on('error', (e)=>{
194
+ outputs[i] += `spawn_error:${e.message}`;
195
+ });
196
+ });
197
+ try {
198
+ await waitFor(()=>existsSync(join(stateRoot, `started-${left}`)) && existsSync(join(stateRoot, `started-${right}`)), 10_000);
199
+ writeFileSync(join(stateRoot, 'start'), 'go');
200
+ await waitFor(()=>existsSync(join(stateRoot, `ready-${left}`)) || existsSync(join(stateRoot, `ready-${right}`)), 10_000);
201
+ await new Promise((resolve)=>setTimeout(resolve, 250));
202
+ assert.equal(existsSync(join(stateRoot, `ready-${left}`)) && existsSync(join(stateRoot, `ready-${right}`)), false, 'only one serialized pending read may pass the barrier');
203
+ } catch (error) {
204
+ children.forEach((p)=>p.kill('SIGKILL'));
205
+ throw new Error(`${error instanceof Error ? error.message : error}:${outputs.join('|')}`);
206
+ }
207
+ writeFileSync(join(stateRoot, 'release'), 'go');
208
+ await Promise.all(children.map((p)=>new Promise((resolve, reject)=>{
209
+ const t = setTimeout(()=>{
210
+ p.kill('SIGKILL');
211
+ reject(new Error('child_process_timeout'));
212
+ }, 10_000);
213
+ p.on('exit', (code)=>{
214
+ clearTimeout(t);
215
+ code === 0 ? resolve() : reject(new Error(`child_exit_${code}:${outputs.join('|')}`));
216
+ });
217
+ })));
218
+ return outputs.map((out)=>JSON.parse(out.trim().split('\n').at(-1)));
219
+ }
220
+ const stateRoot = root ?? mkdtempSync(join(tmpdir(), 'gotry-receipt-concurrency-'));
221
+ if (!child) {
222
+ mkdirSync(stateRoot, {
223
+ recursive: true
224
+ });
225
+ const ledger = ensureLedger(stateRoot);
226
+ const r = new BookingCopilotTaskRuntime(ledger);
227
+ const turn = {
228
+ schemaVersion: 'booking.surface.v1',
229
+ kind: 'user.turn',
230
+ taskId: 'task-concurrency-opaque',
231
+ workspace,
232
+ request: {
233
+ text: 'unique-pii-marker@example.invalid Bearer TEST raw-prompt-marker'
234
+ }
235
+ };
236
+ const sensitiveAction = {
237
+ ...action,
238
+ reason: 'unique-pii-marker@example.invalid Bearer TEST raw-prompt-marker',
239
+ input: {}
240
+ };
241
+ r.startTask(turn);
242
+ r.issueOperation(turn.taskId, sensitiveAction);
243
+ assert.throws(()=>r.continueWithReceipt(continuation({
244
+ ...receipt('a'),
245
+ undoToken: 'malicious@example.invalid'
246
+ })), /invalid_receipt_continuation/);
247
+ assert.throws(()=>r.continueWithReceipt(continuation({
248
+ ...receipt('a'),
249
+ observation: {
250
+ kind: 'gap',
251
+ code: 'raw-prompt-marker user prose',
252
+ factRefs: [
253
+ 'numeric-123'
254
+ ]
255
+ }
256
+ })), /invalid_receipt_continuation/);
257
+ assert.equal(validateActionReceiptV1({
258
+ ...receipt('a'),
259
+ status: 'no_match',
260
+ observation: {
261
+ kind: 'gap',
262
+ code: 'no_match',
263
+ factRefs: [
264
+ '123'
265
+ ]
266
+ }
267
+ }).ok, true);
268
+ const differing = await runPair(stateRoot, 'a', 'b');
269
+ assert.equal(differing.filter((x)=>x.result === 'success').length, 1);
270
+ assert.equal(differing.filter((x)=>x.error === 'receipt_conflict').length, 1);
271
+ const after = ledger;
272
+ assert.equal(after.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 1);
273
+ const persisted = after.db.prepare("SELECT payload FROM events WHERE kind LIKE 'booking.copilot.%'").all();
274
+ assert.ok(!persisted.some((row)=>/unique-pii-marker|Bearer TEST|raw-prompt-marker/i.test(row.payload)));
275
+ const winner = differing.find((x)=>x.result === 'success');
276
+ assert.ok(winner?.variant);
277
+ assert.deepEqual(new BookingCopilotTaskRuntime(after).resumeTask(turn.taskId)?.lastReceipt, receipt(winner.variant));
278
+ after.close();
279
+ rmSync(stateRoot, {
280
+ recursive: true,
281
+ force: true
282
+ });
283
+ const equalRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-equal-'));
284
+ const equalLedger = ensureLedger(equalRoot);
285
+ const equalRuntime = new BookingCopilotTaskRuntime(equalLedger);
286
+ equalRuntime.startTask(turn);
287
+ equalRuntime.issueOperation('task-concurrency-opaque', action);
288
+ const equal = await runPair(equalRoot, 'same-a', 'same-b');
289
+ assert.equal(equal.filter((x)=>x.result === 'success').length, 2);
290
+ const equalCheck = equalLedger;
291
+ assert.equal(equalCheck.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 1);
292
+ const equalPayloads = equalCheck.db.prepare("SELECT payload FROM events WHERE kind LIKE 'booking.copilot.%'").all();
293
+ assert.ok(!equalPayloads.some((row)=>/sensitive|Authorization|Bearer|raw prompt|secret/i.test(row.payload)));
294
+ const tampered = equalCheck.db.prepare("SELECT seq, payload FROM events WHERE kind = 'booking.copilot.receipt.observed' LIMIT 1").get();
295
+ equalCheck.db.prepare('UPDATE events SET payload = ? WHERE seq = ?').run(tampered.payload.replace(/"receiptDigest":"[a-f0-9]+"/, '"receiptDigest":"0000000000000000000000000000000000000000000000000000000000000000"'), tampered.seq);
296
+ assert.throws(()=>new BookingCopilotTaskRuntime(equalCheck).resumeTask('task-concurrency-opaque'), /ledger_corrupt/);
297
+ equalCheck.close();
298
+ rmSync(equalRoot, {
299
+ recursive: true,
300
+ force: true
301
+ });
302
+ for(let round = 0; round < 20; round++){
303
+ const loopRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-round-'));
304
+ const loopLedger = ensureLedger(loopRoot);
305
+ const loopRuntime = new BookingCopilotTaskRuntime(loopLedger);
306
+ loopRuntime.startTask(turn);
307
+ loopRuntime.issueOperation('task-concurrency-opaque', action);
308
+ const result = await runPair(loopRoot, 'a', 'b');
309
+ assert.equal(result.filter((x)=>x.result === 'success').length, 1);
310
+ assert.equal(result.filter((x)=>x.error === 'receipt_conflict').length, 1);
311
+ loopLedger.close();
312
+ rmSync(loopRoot, {
313
+ recursive: true,
314
+ force: true
315
+ });
316
+ }
317
+ const replayRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-replay-'));
318
+ const replayLedger = ensureLedger(replayRoot);
319
+ const replayRuntime = new BookingCopilotTaskRuntime(replayLedger);
320
+ replayRuntime.startTask(turn);
321
+ replayRuntime.issueOperation('task-concurrency-opaque', action);
322
+ replayRuntime.continueWithReceipt(continuation(receipt('same')));
323
+ const nextAction = {
324
+ ...action,
325
+ actionId: 'action-next-opaque',
326
+ expectedRevision: 1
327
+ };
328
+ replayRuntime.issueOperation('task-concurrency-opaque', nextAction);
329
+ const beforeReplay = replayLedger.countEvents();
330
+ const tsxBin = process.env.PATH?.split(':').map((dir)=>join(dir, 'tsx')).find(existsSync);
331
+ assert.ok(tsxBin);
332
+ for (const replayVariant of [
333
+ 'same',
334
+ 'conflict'
335
+ ]){
336
+ const replayChild = spawn(tsxBin, [
337
+ fileURLToPath(import.meta.url),
338
+ '--replay',
339
+ replayRoot,
340
+ replayVariant
341
+ ], {
342
+ cwd: process.cwd(),
343
+ stdio: [
344
+ 'ignore',
345
+ 'pipe',
346
+ 'pipe'
347
+ ]
348
+ });
349
+ let replayOutput = '';
350
+ replayChild.stdout.on('data', (b)=>{
351
+ replayOutput += b.toString();
352
+ });
353
+ replayChild.stderr.on('data', (b)=>{
354
+ replayOutput += b.toString();
355
+ });
356
+ await new Promise((resolve, reject)=>{
357
+ const timer = setTimeout(()=>{
358
+ replayChild.kill('SIGKILL');
359
+ reject(new Error('replay_timeout'));
360
+ }, 10_000);
361
+ replayChild.on('exit', (code)=>{
362
+ clearTimeout(timer);
363
+ code === 0 ? resolve() : reject(new Error(`replay_exit_${code}`));
364
+ });
365
+ });
366
+ const replayResult = JSON.parse(replayOutput.trim().split('\n').at(-1));
367
+ assert.equal(replayResult.result, replayVariant === 'same' ? 'success' : 'error');
368
+ if (replayVariant === 'conflict') assert.equal(replayResult.error, 'receipt_conflict');
369
+ }
370
+ const replayCheck = replayLedger;
371
+ assert.equal(replayCheck.countEvents(), beforeReplay);
372
+ assert.equal(new BookingCopilotTaskRuntime(replayCheck).resumeTask('task-concurrency-opaque')?.pendingAction?.actionId, nextAction.actionId);
373
+ replayCheck.close();
374
+ rmSync(replayRoot, {
375
+ recursive: true,
376
+ force: true
377
+ });
378
+ const reuseRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-reuse-'));
379
+ const reuseLedger = ensureLedger(reuseRoot);
380
+ const reuseRuntime = new BookingCopilotTaskRuntime(reuseLedger);
381
+ reuseRuntime.startTask(turn);
382
+ reuseRuntime.issueOperation('task-concurrency-opaque', action);
383
+ reuseRuntime.continueWithReceipt(continuation(receipt('same')));
384
+ const actionB = {
385
+ ...action,
386
+ actionId: 'action-b-opaque',
387
+ expectedRevision: 1
388
+ };
389
+ reuseRuntime.issueOperation('task-concurrency-opaque', actionB);
390
+ reuseRuntime.continueWithReceipt(continuation({
391
+ ...receipt('same'),
392
+ actionId: actionB.actionId
393
+ }));
394
+ const reuseEvents = reuseLedger.countEvents();
395
+ const reuseState = reuseRuntime.resumeTask('task-concurrency-opaque');
396
+ assert.throws(()=>reuseRuntime.issueOperation('task-concurrency-opaque', action), /action_already_receipted/);
397
+ assert.equal(reuseLedger.countEvents(), reuseEvents);
398
+ assert.deepEqual(reuseRuntime.resumeTask('task-concurrency-opaque'), reuseState);
399
+ reuseLedger.close();
400
+ rmSync(reuseRoot, {
401
+ recursive: true,
402
+ force: true
403
+ });
404
+ const tupleRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-tuple-'));
405
+ const tupleLedger = ensureLedger(tupleRoot);
406
+ const tupleRuntime = new BookingCopilotTaskRuntime(tupleLedger);
407
+ for (const [taskId, actionId] of [
408
+ [
409
+ 'task:a',
410
+ 'b'
411
+ ],
412
+ [
413
+ 'task',
414
+ 'a:b'
415
+ ]
416
+ ]){
417
+ tupleRuntime.startTask({
418
+ ...turn,
419
+ taskId
420
+ });
421
+ tupleRuntime.issueOperation(taskId, {
422
+ ...action,
423
+ actionId
424
+ });
425
+ tupleRuntime.continueWithReceipt(continuation({
426
+ ...receipt('same'),
427
+ actionId
428
+ }, taskId));
429
+ }
430
+ assert.equal(tupleLedger.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.action.issued'").get().n, 2);
431
+ assert.equal(tupleLedger.db.prepare("SELECT COUNT(DISTINCT idem_key) AS n FROM events WHERE kind = 'booking.copilot.action.issued'").get().n, 2);
432
+ assert.equal(tupleLedger.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 2);
433
+ assert.equal(tupleLedger.db.prepare("SELECT COUNT(DISTINCT idem_key) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 2);
434
+ for (const taskId of [
435
+ 'task:a',
436
+ 'task'
437
+ ])assert.equal(tupleRuntime.resumeTask(taskId)?.phase, 'planning');
438
+ tupleLedger.close();
439
+ rmSync(tupleRoot, {
440
+ recursive: true,
441
+ force: true
442
+ });
443
+ console.log('BOOKING COPILOT RECEIPT LEDGER CONCURRENCY PROOF: winner/conflict/atomic persistence OK');
444
+ }
445
+
446
+
447
+ //# sourceURL=ts/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.ts
@@ -0,0 +1,205 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { ensureLedger } from '../src/state-ledger.js';
6
+ import { BookingCopilotTaskRuntime } from '../src/booking-surface/runtime.js';
7
+ const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-copilot-'));
8
+ const ledger = ensureLedger(stateRoot);
9
+ let id = 0;
10
+ const runtimeOptions = {
11
+ idFactory: (prefix)=>`${prefix}-${++id}`,
12
+ now: ()=>'2026-08-30T12:00:00.000Z'
13
+ };
14
+ const strictWorkspace = {
15
+ schemaVersion: 'booking.surface.v1',
16
+ contextRef: 'ctx-server-opaque-1',
17
+ surface: 'tenant',
18
+ revision: 0,
19
+ locale: 'zh-CN',
20
+ currency: 'AED',
21
+ searchDraft: {},
22
+ results: {
23
+ status: 'idle'
24
+ },
25
+ visibleHotels: [],
26
+ loadedOffers: [],
27
+ shortlistedOfferRefs: [],
28
+ capabilities: {
29
+ surface: 'tenant',
30
+ allowedActions: [
31
+ 'search.patch',
32
+ 'search.run',
33
+ 'results.view.patch',
34
+ 'hotel.focus'
35
+ ]
36
+ }
37
+ };
38
+ const startTurn = {
39
+ schemaVersion: 'booking.surface.v1',
40
+ kind: 'user.turn',
41
+ taskId: 'task-opaque-1',
42
+ workspace: {
43
+ ...strictWorkspace,
44
+ capabilities: {
45
+ ...strictWorkspace.capabilities,
46
+ allowedActions: [
47
+ ...strictWorkspace.capabilities.allowedActions
48
+ ]
49
+ }
50
+ },
51
+ request: {
52
+ text: '联系 sensitive@example.com,找 13800001111 附近的酒店'
53
+ }
54
+ };
55
+ const runtime = new BookingCopilotTaskRuntime(ledger, runtimeOptions);
56
+ const started = runtime.startTask(startTurn);
57
+ assert.equal(started.phase, 'planning');
58
+ assert.equal(started.taskId, 'task-opaque-1');
59
+ assert.equal(started.contextRef, strictWorkspace.contextRef);
60
+ assert.equal(started.userTurnCount, 1, 'the initial user turn is a durable task-scoped turn observation');
61
+ const afterRestart = new BookingCopilotTaskRuntime(ledger, runtimeOptions);
62
+ assert.deepEqual(afterRestart.resumeTask('task-opaque-1'), started, 'task resumes from durable ledger state');
63
+ const action = {
64
+ schemaVersion: 'booking.surface.v1',
65
+ kind: 'search.patch',
66
+ actionId: 'action-opaque-1',
67
+ contextRef: strictWorkspace.contextRef,
68
+ expectedRevision: 0,
69
+ reason: 'private villa for sensitive@example.com',
70
+ factRefs: [
71
+ 'fact-opaque-1'
72
+ ],
73
+ input: {
74
+ patch: {
75
+ destination: {
76
+ query: '13800001111 private address'
77
+ }
78
+ }
79
+ }
80
+ };
81
+ const operation = afterRestart.issueOperation('task-opaque-1', action);
82
+ assert.equal(operation.kind, 'operation');
83
+ assert.equal(operation.contextRef, strictWorkspace.contextRef);
84
+ assert.deepEqual(operation.action, action);
85
+ assert.equal(afterRestart.resumeTask('task-opaque-1')?.phase, 'waiting_receipt');
86
+ assert.throws(()=>afterRestart.issueOperation('task-opaque-1', {
87
+ ...action,
88
+ actionId: 'action-opaque-2'
89
+ }), /receipt_required/, 'one task cannot issue another operation before the current receipt');
90
+ assert.throws(()=>afterRestart.startTask({
91
+ ...startTurn,
92
+ request: {
93
+ text: '先继续筛选'
94
+ }
95
+ }), /receipt_required/, 'a fresh user turn cannot bypass the pending operation receipt');
96
+ const receipt = {
97
+ schemaVersion: 'booking.surface.v1',
98
+ kind: 'action.receipt',
99
+ actionId: action.actionId,
100
+ contextRef: strictWorkspace.contextRef,
101
+ status: 'applied',
102
+ revision: 1,
103
+ observation: {
104
+ kind: 'search.state',
105
+ searchSessionRef: 'search-opaque-1',
106
+ resultCount: 12
107
+ },
108
+ resultContract: {
109
+ outcome: 'complete',
110
+ hardCriteriaMet: true,
111
+ factRefs: [
112
+ 'fact-opaque-2'
113
+ ],
114
+ gapCodes: []
115
+ },
116
+ undoToken: 'undo-opaque-1'
117
+ };
118
+ const continuation = {
119
+ schemaVersion: 'booking.surface.v1',
120
+ kind: 'action.receipt.continuation',
121
+ taskId: 'task-opaque-1',
122
+ workspace: {
123
+ ...startTurn.workspace,
124
+ revision: 1,
125
+ results: {
126
+ status: 'ready',
127
+ resultCount: 12,
128
+ searchSessionRef: 'search-opaque-1'
129
+ }
130
+ },
131
+ receipt
132
+ };
133
+ assert.throws(()=>afterRestart.continueWithReceipt({
134
+ ...continuation,
135
+ workspace: {
136
+ ...continuation.workspace,
137
+ contextRef: 'ctx-other'
138
+ },
139
+ receipt: {
140
+ ...receipt,
141
+ contextRef: 'ctx-other'
142
+ }
143
+ }), /context_mismatch/, 'a receipt cannot cross task context');
144
+ const continued = afterRestart.continueWithReceipt(continuation);
145
+ assert.equal(continued.phase, 'planning');
146
+ assert.equal(continued.revision, 1);
147
+ assert.deepEqual(continued.lastReceipt, receipt);
148
+ const eventCountAfterReceipt = ledger.countEvents();
149
+ assert.deepEqual(afterRestart.continueWithReceipt(continuation), continued, 'identical receipt continuation is idempotent');
150
+ assert.equal(ledger.countEvents(), eventCountAfterReceipt, 'duplicate receipt writes no second ledger event');
151
+ const nextUserTurn = {
152
+ ...startTurn,
153
+ workspace: continuation.workspace,
154
+ request: {
155
+ text: '继续筛选,只保留含早和免费取消的报价'
156
+ }
157
+ };
158
+ const continuedTask = afterRestart.startTask(nextUserTurn);
159
+ assert.equal(continuedTask.userTurnCount, 2, 'one task accepts a new user intent after its receipt');
160
+ assert.notEqual(continuedTask.lastUserTurnDigest, started.lastUserTurnDigest, 'each user turn stores its own digest');
161
+ assert.throws(()=>afterRestart.startTask({
162
+ ...nextUserTurn,
163
+ workspace: {
164
+ ...nextUserTurn.workspace,
165
+ revision: 0
166
+ }
167
+ }), /task_conflict:revision_mismatch/, 'a user turn must continue from the current semantic revision');
168
+ assert.throws(()=>afterRestart.startTask({
169
+ ...nextUserTurn,
170
+ workspace: {
171
+ ...nextUserTurn.workspace,
172
+ capabilities: {
173
+ ...nextUserTurn.workspace.capabilities,
174
+ allowedActions: [
175
+ 'search.run'
176
+ ]
177
+ }
178
+ }
179
+ }), /task_conflict:capability_mismatch/, 'a task cannot silently broaden or shrink its surface allowlist');
180
+ const resumedAgain = new BookingCopilotTaskRuntime(ledger, runtimeOptions).resumeTask('task-opaque-1');
181
+ assert.deepEqual(resumedAgain?.lastReceipt, receipt, 'process-style restart recovers the last receipt checkpoint');
182
+ assert.equal(resumedAgain?.userTurnCount, 2, 'process-style restart recovers every user-turn checkpoint');
183
+ const status = new BookingCopilotTaskRuntime(ledger, runtimeOptions).emitEvent('task-opaque-1', {
184
+ kind: 'status',
185
+ status: 'working'
186
+ });
187
+ assert.ok(status.sequence > operation.sequence, 'event sequence remains monotonic after runtime restart');
188
+ const persisted = ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind LIKE 'booking.copilot.%' ORDER BY seq`).all(ledger.tenant, 'task-opaque-1');
189
+ const persistedText = persisted.map((row)=>row.payload).join('\n');
190
+ assert.ok(!persistedText.includes('sensitive@example.com'), 'raw user/action prose is not persisted');
191
+ assert.ok(!persistedText.includes('13800001111'), 'raw criteria payload is not persisted');
192
+ assert.ok(!persistedText.includes('private villa'), 'action reason is persisted only as a digest');
193
+ assert.match(persistedText, /requestDigest/, 'redacted request digest is durable');
194
+ assert.match(persistedText, /inputDigest/, 'redacted action input digest is durable');
195
+ const userTurnCountRow = ledger.db.prepare(`SELECT COUNT(*) AS count FROM events WHERE tenant_id = ? AND run_id = ? AND kind = 'booking.copilot.user.turn.observed'`).get(ledger.tenant, 'task-opaque-1');
196
+ assert.equal(Number(userTurnCountRow?.count ?? 0), 2, 'task.started is separate from one digest-only row per accepted user turn');
197
+ ledger.close();
198
+ rmSync(stateRoot, {
199
+ recursive: true,
200
+ force: true
201
+ });
202
+ console.log('BOOKING COPILOT RUNTIME PROOF: task/operation/receipt/restart/idempotency/redaction OK');
203
+
204
+
205
+ //# sourceURL=ts/scripts/booking-copilot-runtime-proof-tests.ts