codex-chatgpt-control 0.5.1-alpha.2 → 0.5.1-alpha.3

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 (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. package/references/streaming.md +6 -0
@@ -0,0 +1,1039 @@
1
+ import { decideOperationRecovery } from "./recovery.js";
2
+ import { assertOperationStateShape } from "./state-machine.js";
3
+ import { OPERATION_RECOVERY_OBSERVATION_SCHEMA_VERSION, OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION, OPERATION_RECEIPT_SCHEMA_VERSION, OPERATION_HANDLE_SCHEMA_VERSION } from "./types.js";
4
+ import { classifyTurnOwnership, TURN_OWNERSHIP_SCHEMA_VERSION } from "./turn-ownership.js";
5
+ import { canonicalJson } from "./canonical.js";
6
+ /**
7
+ * The collector is intentionally a read-only orchestration boundary. There
8
+ * is no composer, file-transfer, click, send, stop, or steer port here. A
9
+ * browser adapter implements one bounded observation transaction; timers and
10
+ * journal reads are separate operations and never run inside that transaction.
11
+ *
12
+ * The adapter contract is deliberately normalized: it supplies ownership
13
+ * digests and bounded metadata, never DOM nodes, prompts, file paths, URLs, or
14
+ * artifact bytes. It may supply raw response text only for the exact terminal
15
+ * turn in the current collect call; the persistence port never receives it.
16
+ */
17
+ export const COLLECTOR_SCHEMA_VERSION = "chatgpt.browser_control.collector.v1";
18
+ export const COLLECTOR_TERMINAL_SCHEMA_VERSION = "chatgpt.browser_control.collector_terminal.v1";
19
+ const MAX_ATTEMPTS = 32;
20
+ const MAX_ARTIFACTS = 32;
21
+ const MAX_FINISH_REASON_LENGTH = 128;
22
+ const MAX_MIME_LENGTH = 128;
23
+ const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
24
+ const MAX_RESPONSE_CHARS = 8 * 1024 * 1024;
25
+ const HMAC_DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
26
+ const CONTENT_DIGEST_PATTERN = /^(?:hmac-sha256:|sha256:)[0-9a-f]{64}$/;
27
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
28
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/;
29
+ const TRANSFER_OUTPUT_KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
30
+ const BLOCKER_CODE_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
31
+ const PHASES = new Set([
32
+ "prepared", "handoff_pending", "ready", "send_pending", "submitted",
33
+ "generating", "capturing", "completed", "uncertain"
34
+ ]);
35
+ const BOUNDARIES = new Set([
36
+ "none", "handoff_may_have_occurred", "send_may_have_occurred", "control_may_have_occurred"
37
+ ]);
38
+ /**
39
+ * Collect an exact operation-owned assistant turn. Polling is optional and
40
+ * bounded. Each loop performs a browser-free journal read followed by one
41
+ * short observation transaction, then sleeps outside that transaction.
42
+ */
43
+ export async function collectOperation(handle, ports, options = {}) {
44
+ const identity = safeHandleIdentity(handle);
45
+ let settings;
46
+ try {
47
+ settings = normalizeOptions(options);
48
+ }
49
+ catch {
50
+ return blocked(identity, "port_protocol_violation", 0, "prepared", "none");
51
+ }
52
+ const signal = settings.signal;
53
+ if (signal.aborted)
54
+ return blocked(identity, "operation_cancelled", 0, "prepared", "none");
55
+ try {
56
+ validateHandle(handle);
57
+ validatePorts(ports);
58
+ }
59
+ catch {
60
+ return blocked(identity, "port_protocol_violation", 0, "prepared", "none");
61
+ }
62
+ const startedAt = settings.now();
63
+ const deadlineAt = startedAt + settings.timeoutMs;
64
+ let attempts = 0;
65
+ while (attempts < settings.maxAttempts) {
66
+ if (signal.aborted)
67
+ return blocked(identity, "operation_cancelled", attempts, "prepared", "none");
68
+ if (settings.now() >= deadlineAt)
69
+ return blocked(identity, "operation_timeout", attempts, "prepared", "none");
70
+ attempts += 1;
71
+ let durable;
72
+ try {
73
+ durable = await ports.readDurable({ handle });
74
+ validateDurable(handle, durable);
75
+ }
76
+ catch (error) {
77
+ if (signal.aborted)
78
+ return blocked(identity, "operation_cancelled", attempts, "prepared", "none");
79
+ if (settings.now() >= deadlineAt)
80
+ return blocked(identity, "operation_timeout", attempts, "prepared", "none");
81
+ const durableError = collectorDurableErrorCode(error);
82
+ if (durableError !== undefined) {
83
+ return blocked(identity, durableError, attempts, handle.phase, handle.mutationBoundary, handle.targetBindingDigest);
84
+ }
85
+ return blocked(identity, "port_protocol_violation", attempts, "prepared", "none");
86
+ }
87
+ let state = durable.state;
88
+ if (state.capturePolicy !== undefined && settings.responseContent !== state.capturePolicy.responseContent) {
89
+ // Response-content policy is immutable. In particular, do not allow a
90
+ // later collect call to escalate a metadata-only request into raw text.
91
+ return blocked(identity, "operation_request_mismatch", attempts, state.phase, state.mutationBoundary, handle.targetBindingDigest);
92
+ }
93
+ const durableResponseFormat = state.capturePolicy?.responseFormat ?? state.responseFormat;
94
+ if (settings.responseFormat !== undefined
95
+ && durableResponseFormat !== undefined
96
+ && settings.responseFormat !== durableResponseFormat) {
97
+ return blocked(identity, "operation_request_mismatch", attempts, state.phase, state.mutationBoundary, handle.targetBindingDigest);
98
+ }
99
+ // A legacy durable state omitted the format and therefore has the
100
+ // historical Markdown identity. Do not let a later explicit text
101
+ // request silently change that operation's capture semantics.
102
+ if (settings.responseFormat === "text" && durableResponseFormat === undefined) {
103
+ return blocked(identity, "operation_request_mismatch", attempts, state.phase, state.mutationBoundary, handle.targetBindingDigest);
104
+ }
105
+ // New durable states carry the immutable submit-time format. Legacy
106
+ // states omit it and retain the adapter's historical Markdown default.
107
+ const responseFormat = settings.responseFormat ?? durableResponseFormat;
108
+ if (state.phase === "completed") {
109
+ return completedFromReceipt(identity, handle, state.receipt, attempts);
110
+ }
111
+ if (!isCollectableState(state)) {
112
+ return blockedForState(identity, handle, state, attempts, "operation_not_collectable");
113
+ }
114
+ let observation;
115
+ try {
116
+ observation = await ports.observe({
117
+ operationId: state.operationId,
118
+ requestDigest: state.requestDigest,
119
+ targetBindingDigest: handle.targetBindingDigest ?? durable.binding.targetBindingDigest,
120
+ responseContent: settings.responseContent,
121
+ ...(responseFormat === undefined ? {} : { responseFormat }),
122
+ signal,
123
+ deadlineAt
124
+ });
125
+ validateObservation(observation, settings.responseContent, responseFormat);
126
+ }
127
+ catch {
128
+ if (signal.aborted)
129
+ return blockedForState(identity, handle, state, attempts, "operation_cancelled");
130
+ if (settings.now() >= deadlineAt)
131
+ return blockedForState(identity, handle, state, attempts, "operation_timeout");
132
+ return blockedForState(identity, handle, state, attempts, "port_protocol_violation");
133
+ }
134
+ if (signal.aborted)
135
+ return blockedForState(identity, handle, state, attempts, "operation_cancelled");
136
+ if (settings.now() >= deadlineAt)
137
+ return blockedForState(identity, handle, state, attempts, "operation_timeout");
138
+ let classification;
139
+ try {
140
+ classification = classifyTurnOwnership({
141
+ binding: durable.binding,
142
+ baseline: durable.baseline,
143
+ snapshot: observation.snapshot,
144
+ ...(durable.submissionWitness === undefined ? {} : { submissionWitness: durable.submissionWitness }),
145
+ ...(durable.prior === undefined ? {} : { prior: durable.prior })
146
+ });
147
+ }
148
+ catch {
149
+ return blockedForState(identity, handle, state, attempts, "port_protocol_violation");
150
+ }
151
+ const recoveryObservation = toRecoveryObservation(classification);
152
+ const decision = decideOperationRecovery(state, recoveryObservation);
153
+ if (decision.kind === "capture_owned_turn") {
154
+ if (classification.status !== "owned_assistant_terminal" || observation.terminal === undefined) {
155
+ return blockedForState(identity, handle, state, attempts, "capture_ownership_lost", classification.evidence.snapshotDigest);
156
+ }
157
+ try {
158
+ validateTerminalOwnership(observation.terminal, classification);
159
+ }
160
+ catch {
161
+ return blockedForState(identity, handle, state, attempts, "port_protocol_violation");
162
+ }
163
+ let receipt;
164
+ try {
165
+ receipt = terminalReceiptFromObservation(identity, durable, classification, observation.terminal, settings.now());
166
+ }
167
+ catch {
168
+ return blockedForState(identity, handle, state, attempts, "port_protocol_violation", classification.evidence.snapshotDigest);
169
+ }
170
+ if (signal.aborted)
171
+ return blockedForState(identity, handle, state, attempts, "operation_cancelled", classification.evidence.snapshotDigest);
172
+ if (settings.now() >= deadlineAt)
173
+ return blockedForState(identity, handle, state, attempts, "operation_timeout", classification.evidence.snapshotDigest);
174
+ let persisted;
175
+ try {
176
+ persisted = await ports.persistTerminal({
177
+ durable,
178
+ receipt,
179
+ signal,
180
+ deadlineAt
181
+ });
182
+ validateDurable(handle, persisted);
183
+ if (persisted.state.phase !== "completed" || persisted.state.receipt === undefined) {
184
+ throw new Error("terminal persistence did not return a completed durable receipt");
185
+ }
186
+ assertConvergedTerminalReceipt(persisted.state.receipt, receipt);
187
+ }
188
+ catch (error) {
189
+ // Once persistence begins, a thrown result cannot tell us whether the
190
+ // journal committed before the failure. Never retry or report a
191
+ // synthetic completion from this invocation.
192
+ return blockedForState(identity, handle, state, attempts, collectorPersistenceErrorCode(error), classification.evidence.snapshotDigest);
193
+ }
194
+ const durableResult = completedFromReceipt(identity, handle, persisted.state.receipt, attempts);
195
+ return addLiveContent(durableResult, observation.terminal, classification);
196
+ }
197
+ if (decision.kind === "continue_owned_turn_observation"
198
+ && decision.evidenceDigest !== undefined
199
+ && progressNeedsPersistence(state.phase, decision.phase)) {
200
+ try {
201
+ durable = await ports.persistProgress({
202
+ durable,
203
+ phase: decision.phase,
204
+ evidenceDigest: decision.evidenceDigest,
205
+ signal,
206
+ deadlineAt
207
+ });
208
+ validateDurable(handle, durable);
209
+ if (!progressPhaseReached(durable.state.phase, decision.phase)) {
210
+ throw new Error("progress persistence did not reach the proven ownership phase");
211
+ }
212
+ state = durable.state;
213
+ }
214
+ catch {
215
+ return blockedForState(identity, handle, state, attempts, "operation_progress_persistence_failed", classification.evidence.snapshotDigest);
216
+ }
217
+ }
218
+ const blocker = blockerForDecision(decision, classification, state, identity, handle, attempts);
219
+ if (blocker !== undefined)
220
+ return blocker;
221
+ if (!settings.wait)
222
+ return pending(identity, handle, durable.binding.targetBindingDigest, state, attempts);
223
+ if (attempts >= settings.maxAttempts)
224
+ return blockedForState(identity, handle, state, attempts, "operation_timeout");
225
+ if (signal.aborted)
226
+ return blockedForState(identity, handle, state, attempts, "operation_cancelled");
227
+ const remaining = Math.max(0, deadlineAt - settings.now());
228
+ if (remaining <= 0)
229
+ return blockedForState(identity, handle, state, attempts, "operation_timeout");
230
+ const delay = Math.min(settings.pollIntervalMs, remaining);
231
+ try {
232
+ await ports.sleep(delay, signal);
233
+ }
234
+ catch {
235
+ return blockedForState(identity, handle, state, attempts, signal.aborted ? "operation_cancelled" : "port_protocol_violation");
236
+ }
237
+ }
238
+ return blocked(identity, "operation_timeout", attempts, "prepared", "none");
239
+ }
240
+ function normalizeOptions(options) {
241
+ const signal = options.signal ?? new AbortController().signal;
242
+ const now = options.now ?? Date.now;
243
+ const timeoutMs = options.timeoutMs ?? 30_000;
244
+ const maxAttempts = options.maxAttempts ?? MAX_ATTEMPTS;
245
+ const pollIntervalMs = options.pollIntervalMs ?? 250;
246
+ if (typeof options.wait !== "undefined" && typeof options.wait !== "boolean")
247
+ throw new TypeError("Invalid collector wait option.");
248
+ if (options.responseContent !== undefined && options.responseContent !== "include" && options.responseContent !== "metadata")
249
+ throw new TypeError("Invalid collector response-content option.");
250
+ if (options.responseFormat !== undefined && options.responseFormat !== "markdown" && options.responseFormat !== "text")
251
+ throw new TypeError("Invalid collector response-format option.");
252
+ if (!isAbortSignal(signal))
253
+ throw new TypeError("Invalid collector cancellation signal.");
254
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86_400_000)
255
+ throw new TypeError("Invalid collector timeout.");
256
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > MAX_ATTEMPTS)
257
+ throw new TypeError("Invalid collector attempt bound.");
258
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 0 || pollIntervalMs > 60_000)
259
+ throw new TypeError("Invalid collector poll interval.");
260
+ if (typeof now !== "function")
261
+ throw new TypeError("Invalid collector clock.");
262
+ const checkedNow = () => {
263
+ const value = now();
264
+ if (!Number.isFinite(value))
265
+ throw new TypeError("Collector clock returned a non-finite value.");
266
+ return value;
267
+ };
268
+ return {
269
+ signal,
270
+ wait: options.wait ?? false,
271
+ timeoutMs,
272
+ maxAttempts,
273
+ pollIntervalMs,
274
+ responseContent: options.responseContent ?? "include",
275
+ ...(options.responseFormat === undefined ? {} : { responseFormat: options.responseFormat }),
276
+ now: checkedNow
277
+ };
278
+ }
279
+ function validateHandle(handle) {
280
+ assertExactKeys(handle, "operation handle", ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary", "targetBindingDigest"], ["schemaVersion", "operationId", "requestDigest", "surface", "revision", "phase", "mutationBoundary"]);
281
+ if (handle.schemaVersion !== OPERATION_HANDLE_SCHEMA_VERSION)
282
+ throw new TypeError("Unsupported operation handle schema.");
283
+ assertOperationId(handle.operationId, "operationId");
284
+ assertDigest(handle.requestDigest, "requestDigest");
285
+ if (handle.surface !== "chat" && handle.surface !== "work")
286
+ throw new TypeError("Invalid operation surface.");
287
+ if (!Number.isSafeInteger(handle.revision) || handle.revision < 1)
288
+ throw new TypeError("Invalid operation revision.");
289
+ if (!PHASES.has(handle.phase) || !BOUNDARIES.has(handle.mutationBoundary))
290
+ throw new TypeError("Invalid operation progress.");
291
+ if (handle.targetBindingDigest !== undefined)
292
+ assertDigest(handle.targetBindingDigest, "targetBindingDigest");
293
+ }
294
+ function validatePorts(ports) {
295
+ if (!isRecord(ports) || typeof ports.readDurable !== "function" || typeof ports.observe !== "function" || typeof ports.persistProgress !== "function" || typeof ports.persistTerminal !== "function" || typeof ports.sleep !== "function")
296
+ throw new TypeError("Collector ports are incomplete.");
297
+ assertExactKeys(ports, "collector ports", ["readDurable", "observe", "persistProgress", "persistTerminal", "sleep"], ["readDurable", "observe", "persistProgress", "persistTerminal", "sleep"]);
298
+ }
299
+ function validateDurable(handle, durable) {
300
+ if (!isRecord(durable))
301
+ throw new TypeError("Durable collector snapshot must be an object.");
302
+ assertExactKeys(durable, "durable collector snapshot", ["state", "binding", "baseline", "submissionWitness", "prior"], ["state", "binding", "baseline"]);
303
+ assertOperationStateShape(durable.state);
304
+ if (durable.state.operationId !== handle.operationId || durable.state.requestDigest !== handle.requestDigest || durable.state.surface !== handle.surface)
305
+ throw new TypeError("Durable state does not match handle.");
306
+ if (durable.state.revision < handle.revision)
307
+ throw new TypeError("Durable state is older than handle.");
308
+ const boundaryRank = { none: 0, handoff_may_have_occurred: 1, send_may_have_occurred: 2, control_may_have_occurred: 3 };
309
+ if (boundaryRank[handle.mutationBoundary] > boundaryRank[durable.state.mutationBoundary])
310
+ throw new TypeError("Handle claims a mutation boundary ahead of durable state.");
311
+ if (handle.revision === durable.state.revision && (handle.phase !== durable.state.phase || handle.mutationBoundary !== durable.state.mutationBoundary))
312
+ throw new TypeError("Handle progress disagrees with durable state.");
313
+ if (durable.state.target === undefined)
314
+ throw new TypeError("Collectable durable state is missing its target binding.");
315
+ if (handle.targetBindingDigest !== undefined && durable.binding.targetBindingDigest !== handle.targetBindingDigest)
316
+ throw new TypeError("Durable target binding does not match handle.");
317
+ if (durable.binding.operationId !== handle.operationId || durable.binding.targetBindingDigest !== (handle.targetBindingDigest ?? durable.binding.targetBindingDigest))
318
+ throw new TypeError("Durable ownership binding does not match handle.");
319
+ const submissionAction = Object.values(durable.state.actions)
320
+ .find(action => action.kind === "send");
321
+ if (submissionAction?.targetDigest !== durable.binding.targetBindingDigest)
322
+ throw new TypeError("Durable submission target does not match ownership binding.");
323
+ const postSendPhase = durable.state.phase === "submitted"
324
+ || durable.state.phase === "generating"
325
+ || durable.state.phase === "capturing"
326
+ || durable.state.phase === "completed";
327
+ if (postSendPhase && durable.state.submissionWitness === undefined) {
328
+ throw new TypeError("Durable post-Send state is missing its submission witness.");
329
+ }
330
+ if (durable.state.submissionWitness !== undefined) {
331
+ const persisted = durable.state.submissionWitness;
332
+ const establishment = durable.state.target?.targetEstablishment;
333
+ if (establishment !== undefined && establishment.causalSendActionId === persisted.actionId) {
334
+ if (establishment.postSendDeltaDigest !== undefined
335
+ && establishment.postSendDeltaDigest !== persisted.postSendDeltaDigest)
336
+ throw new TypeError("Durable target establishment disagrees with the submission witness.");
337
+ if (establishment.userTurnEvidenceDigest !== persisted.operationUserEvidenceDigest) {
338
+ throw new TypeError("Durable target establishment user evidence disagrees with the submission witness.");
339
+ }
340
+ }
341
+ }
342
+ if (durable.baseline.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION)
343
+ throw new TypeError("Unsupported ownership baseline schema.");
344
+ validateDurableTarget(durable.state, durable.binding);
345
+ validateOwnershipEnvelope(durable.binding, durable.baseline);
346
+ const active = activeDurableOwnership(durable.state, durable.binding);
347
+ if (active === undefined) {
348
+ throw new TypeError("Durable ownership baseline and witness are missing for the active action.");
349
+ }
350
+ if (canonicalJson(active.baseline.baseline) !== canonicalJson(durable.baseline)) {
351
+ throw new TypeError("Collector baseline is not the authenticated per-action baseline.");
352
+ }
353
+ if (durable.submissionWitness === undefined || !matchesCollectorWitness(durable.submissionWitness, active.witness)) {
354
+ throw new TypeError("Collector submission witness is not the authenticated per-action witness.");
355
+ }
356
+ }
357
+ /**
358
+ * Select the authenticated ownership records for the action projected by the
359
+ * collector binding. New state carries both records in keyed maps. The
360
+ * only compatibility fallback is the original Send projection in a legacy
361
+ * state that predates those maps; in particular, a missing Work key is never
362
+ * silently replaced by Send evidence.
363
+ */
364
+ function activeDurableOwnership(state, binding) {
365
+ const action = state.actions[binding.actionId];
366
+ if (action === undefined || action.kind !== binding.actionKind || action.targetDigest !== binding.targetBindingDigest) {
367
+ throw new TypeError("Collector binding does not name the authenticated durable action.");
368
+ }
369
+ if (action.outcome === "not_satisfied" || action.outcome === "uncertain") {
370
+ throw new TypeError("Collector binding names an action without a satisfied ownership proof.");
371
+ }
372
+ const baseline = state.ownershipBaselines === undefined
373
+ ? binding.actionKind === "send" && state.ownershipBaseline?.actionId === binding.actionId
374
+ ? state.ownershipBaseline
375
+ : undefined
376
+ : state.ownershipBaselines[binding.actionId];
377
+ const witness = state.submissionWitnesses === undefined
378
+ ? binding.actionKind === "send" && state.submissionWitness?.actionId === binding.actionId
379
+ ? state.submissionWitness
380
+ : undefined
381
+ : state.submissionWitnesses[binding.actionId];
382
+ if (baseline === undefined || witness === undefined)
383
+ return undefined;
384
+ if (baseline.operationId !== state.operationId
385
+ || baseline.requestDigest !== state.requestDigest
386
+ || baseline.targetBindingDigest !== binding.targetBindingDigest
387
+ || baseline.actionId !== binding.actionId
388
+ || baseline.baseline.snapshotDigest !== witness.baselineSnapshotDigest) {
389
+ throw new TypeError("Collector ownership records disagree with their authenticated action.");
390
+ }
391
+ if (witness.actionId !== binding.actionId || witness.actionKind !== binding.actionKind || witness.targetBindingDigest !== binding.targetBindingDigest) {
392
+ throw new TypeError("Collector witness does not match the authenticated action.");
393
+ }
394
+ return { baseline, witness };
395
+ }
396
+ function matchesCollectorWitness(projected, persisted) {
397
+ return projected.actionId === persisted.actionId
398
+ && projected.actionKind === persisted.actionKind
399
+ && projected.baselineSnapshotDigest === persisted.baselineSnapshotDigest
400
+ && projected.postSendDeltaDigest === persisted.postSendDeltaDigest
401
+ && projected.operationUserEvidenceDigest === persisted.operationUserEvidenceDigest
402
+ && (persisted.userTurnId === undefined
403
+ ? projected.userTurnStableId === undefined
404
+ : projected.userTurnStableId === persisted.userTurnId);
405
+ }
406
+ function validateDurableTarget(state, binding) {
407
+ const target = state.target;
408
+ if (target === undefined || target.coordinationScope !== binding.target.coordinationScope)
409
+ throw new TypeError("Durable and ownership target scopes differ.");
410
+ const exact = (stateValue, ownershipValue) => {
411
+ if (ownershipValue.status === "available" && ownershipValue.value !== stateValue)
412
+ throw new TypeError("Durable and ownership target identities differ.");
413
+ };
414
+ exact(target.providerId, binding.target.provider);
415
+ exact(target.browserId, binding.target.browser);
416
+ exact(target.tabId, binding.target.tab);
417
+ if (binding.target.conversation.status === "available" && target.conversationId !== binding.target.conversation.value)
418
+ throw new TypeError("Durable and ownership conversation identities differ.");
419
+ if (binding.evidenceProfile.stableConversationId === "required" && target.conversationId === undefined)
420
+ throw new TypeError("Durable target lacks required conversation identity.");
421
+ }
422
+ function validateOwnershipEnvelope(binding, baseline) {
423
+ const baselineSnapshot = {
424
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
425
+ snapshotDigest: baseline.snapshotDigest,
426
+ target: baseline.target,
427
+ userTurns: baseline.userTurns,
428
+ assistantTurns: baseline.assistantTurns,
429
+ completeness: "complete",
430
+ terminalState: "idle"
431
+ };
432
+ classifyTurnOwnership({ binding, baseline, snapshot: baselineSnapshot });
433
+ }
434
+ function validateObservation(observation, responseContent, responseFormat) {
435
+ if (!isRecord(observation))
436
+ throw new TypeError("Collector observation must be an object.");
437
+ assertExactKeys(observation, "collector observation", ["schemaVersion", "snapshot", "terminal"], ["schemaVersion", "snapshot"]);
438
+ if (observation.schemaVersion !== COLLECTOR_SCHEMA_VERSION)
439
+ throw new TypeError("Unsupported collector observation schema.");
440
+ if (!isRecord(observation.snapshot) || observation.snapshot.schemaVersion !== TURN_OWNERSHIP_SCHEMA_VERSION)
441
+ throw new TypeError("Invalid ownership snapshot.");
442
+ if (observation.terminal !== undefined) {
443
+ if (observation.snapshot.terminalState !== "terminal")
444
+ throw new TypeError("A terminal observation requires a terminal ownership snapshot.");
445
+ validateTerminalObservationShape(observation.terminal, responseContent, responseFormat);
446
+ }
447
+ }
448
+ function validateTerminalObservationShape(terminal, responseContent, responseFormat) {
449
+ assertExactKeys(terminal, "collector terminal observation", ["schemaVersion", "userTurnId", "assistantTurnId", "userTurnEvidenceDigest", "assistantTurnEvidenceDigest", "userOrdinal", "assistantOrdinal", "branchStableId", "text", "responseFormat", "rawText", "artifacts", "finishReason"], ["schemaVersion", "userTurnId", "assistantTurnId", "userTurnEvidenceDigest", "assistantTurnEvidenceDigest", "userOrdinal", "assistantOrdinal", "branchStableId", "artifacts", "finishReason"]);
450
+ if (terminal.schemaVersion !== COLLECTOR_TERMINAL_SCHEMA_VERSION)
451
+ throw new TypeError("Unsupported terminal observation schema.");
452
+ assertId(terminal.userTurnId, "terminal.userTurnId");
453
+ assertId(terminal.assistantTurnId, "terminal.assistantTurnId");
454
+ assertDigest(terminal.userTurnEvidenceDigest, "terminal.userTurnEvidenceDigest");
455
+ assertDigest(terminal.assistantTurnEvidenceDigest, "terminal.assistantTurnEvidenceDigest");
456
+ assertId(terminal.branchStableId, "terminal.branchStableId");
457
+ assertOrdinal(terminal.userOrdinal, "terminal.userOrdinal");
458
+ assertOrdinal(terminal.assistantOrdinal, "terminal.assistantOrdinal");
459
+ if (terminal.responseFormat !== undefined && terminal.responseFormat !== "markdown" && terminal.responseFormat !== "text")
460
+ throw new TypeError("Invalid terminal response format.");
461
+ if (responseFormat !== undefined && terminal.responseFormat !== responseFormat)
462
+ throw new TypeError("Terminal response format does not match the immutable request.");
463
+ if (!Array.isArray(terminal.artifacts) || terminal.artifacts.length > MAX_ARTIFACTS)
464
+ throw new TypeError("Terminal artifacts exceed the bounded cap.");
465
+ for (let index = 0; index < terminal.artifacts.length; index += 1) {
466
+ const artifact = terminal.artifacts[index];
467
+ if (artifact === undefined)
468
+ throw new TypeError("Missing terminal artifact.");
469
+ validateArtifact(artifact, index);
470
+ }
471
+ if (typeof terminal.finishReason !== "string" || terminal.finishReason.length === 0 || terminal.finishReason.length > MAX_FINISH_REASON_LENGTH || /[\u0000-\u001f\u007f]/u.test(terminal.finishReason))
472
+ throw new TypeError("Invalid terminal finish reason.");
473
+ if (terminal.text !== undefined)
474
+ validateTextDigest(terminal.text);
475
+ if (terminal.rawText !== undefined) {
476
+ if (responseContent !== "include")
477
+ throw new TypeError("Raw response content is not allowed for metadata collection.");
478
+ if (typeof terminal.rawText !== "string" || terminal.rawText.length > MAX_RESPONSE_CHARS || terminal.rawText.includes("\u0000"))
479
+ throw new TypeError("Raw response content exceeds the bounded collector limit.");
480
+ if (terminal.text === undefined)
481
+ throw new TypeError("Raw response content requires response metadata.");
482
+ const bytes = Buffer.byteLength(terminal.rawText, "utf8");
483
+ if (bytes > MAX_RESPONSE_BYTES || terminal.text.bytes !== bytes)
484
+ throw new TypeError("Raw response content bytes do not match its metadata.");
485
+ if (terminal.text.chars !== undefined && terminal.text.chars !== terminal.rawText.length)
486
+ throw new TypeError("Raw response content character count does not match its metadata.");
487
+ }
488
+ }
489
+ function validateTextDigest(text) {
490
+ assertExactKeys(text, "collector text digest", ["digest", "bytes", "chars"], ["digest"]);
491
+ assertDigest(text.digest, "text.digest");
492
+ if (text.bytes !== undefined) {
493
+ assertNonNegativeSafeInteger(text.bytes, "text.bytes");
494
+ if (text.bytes > MAX_RESPONSE_BYTES)
495
+ throw new TypeError("text.bytes exceeds the bounded collector limit.");
496
+ }
497
+ if (text.chars !== undefined) {
498
+ assertNonNegativeSafeInteger(text.chars, "text.chars");
499
+ if (text.chars > MAX_RESPONSE_CHARS)
500
+ throw new TypeError("text.chars exceeds the bounded collector limit.");
501
+ }
502
+ if (text.bytes === undefined && text.chars === undefined)
503
+ throw new TypeError("Response metadata requires a byte or character count.");
504
+ }
505
+ function validateArtifact(artifact, expectedOrdinal) {
506
+ assertExactKeys(artifact, "collector artifact", ["kind", "ordinal", "sourceIdentityDigest", "contentDigest", "bytes", "mimeType"], ["kind", "ordinal", "sourceIdentityDigest"]);
507
+ if (artifact.kind !== "file" && artifact.kind !== "image" && artifact.kind !== "other")
508
+ throw new TypeError("Invalid artifact kind.");
509
+ assertOrdinal(artifact.ordinal, "artifact.ordinal");
510
+ if (artifact.ordinal !== expectedOrdinal)
511
+ throw new TypeError("Artifact ordinals must be contiguous and ordered.");
512
+ assertDigest(artifact.sourceIdentityDigest, "artifact.sourceIdentityDigest");
513
+ if (artifact.contentDigest !== undefined)
514
+ assertContentDigest(artifact.contentDigest, "artifact.contentDigest");
515
+ if (artifact.bytes !== undefined)
516
+ assertNonNegativeSafeInteger(artifact.bytes, "artifact.bytes");
517
+ if (artifact.mimeType !== undefined && (typeof artifact.mimeType !== "string" || artifact.mimeType.length === 0 || artifact.mimeType.length > MAX_MIME_LENGTH || /[\u0000-\u001f\u007f]/u.test(artifact.mimeType)))
518
+ throw new TypeError("Invalid artifact MIME type.");
519
+ }
520
+ function validateTerminalOwnership(terminal, classification) {
521
+ const user = classification.evidence.userTurn;
522
+ const assistant = classification.evidence.assistantTurn;
523
+ if (classification.status !== "owned_assistant_terminal" || user === undefined || assistant === undefined || classification.cursor?.phase !== "owned_assistant_terminal")
524
+ throw new TypeError("Terminal ownership is not proven.");
525
+ if (terminal.userTurnId !== classification.cursor.userTurnId || terminal.assistantTurnId !== classification.cursor.assistantTurnId)
526
+ throw new TypeError("Terminal turn identity mismatch.");
527
+ if (terminal.userTurnEvidenceDigest !== user.evidenceDigest || terminal.assistantTurnEvidenceDigest !== assistant.evidenceDigest)
528
+ throw new TypeError("Terminal evidence mismatch.");
529
+ if (terminal.userOrdinal !== user.ordinal || terminal.assistantOrdinal !== assistant.ordinal)
530
+ throw new TypeError("Terminal ordinal mismatch.");
531
+ if (terminal.branchStableId !== classification.cursor.assistantBranchId)
532
+ throw new TypeError("Terminal branch mismatch.");
533
+ const expectedArtifacts = assistant.artifactEvidenceDigests;
534
+ if (terminal.artifacts.length !== expectedArtifacts.length || terminal.artifacts.some((artifact, index) => artifact.sourceIdentityDigest !== expectedArtifacts[index]))
535
+ throw new TypeError("Terminal artifact ownership mismatch.");
536
+ }
537
+ function toRecoveryObservation(classification) {
538
+ const target = classification.status === "target_mismatch"
539
+ ? { status: "mismatch", evidenceDigest: classification.evidence.snapshotDigest }
540
+ : classification.status === "target_evidence_unavailable"
541
+ ? { status: "unavailable", evidenceDigest: classification.evidence.snapshotDigest }
542
+ : { status: "matches" };
543
+ const recovery = classification.recoveryObservation;
544
+ let turn;
545
+ switch (recovery.status) {
546
+ case "not_observed":
547
+ turn = { status: "not_observed" };
548
+ break;
549
+ case "owned_user_turn":
550
+ turn = { status: "owned_user_turn", userTurnId: recovery.userTurnId, evidenceDigest: recovery.evidenceDigest };
551
+ break;
552
+ case "owned_assistant_generating":
553
+ turn = { status: "owned_assistant_generating", userTurnId: recovery.userTurnId, assistantTurnId: recovery.assistantTurnId, evidenceDigest: recovery.evidenceDigest };
554
+ break;
555
+ case "owned_assistant_terminal":
556
+ turn = { status: "owned_assistant_terminal", userTurnId: recovery.userTurnId, assistantTurnId: recovery.assistantTurnId, evidenceDigest: recovery.evidenceDigest };
557
+ break;
558
+ case "ambiguous":
559
+ turn = { status: "ambiguous", ...(recovery.evidenceDigest === undefined ? {} : { evidenceDigest: recovery.evidenceDigest }) };
560
+ break;
561
+ }
562
+ return { schemaVersion: OPERATION_RECOVERY_OBSERVATION_SCHEMA_VERSION, target, turn };
563
+ }
564
+ function blockerForDecision(decision, classification, state, identity, handle, attempts) {
565
+ if (classification.status === "concurrent_user_turn")
566
+ return blockedForState(identity, handle, state, attempts, "concurrent_user_turn", classification.evidence.snapshotDigest);
567
+ if (classification.status === "regeneration_ambiguous")
568
+ return blockedForState(identity, handle, state, attempts, "regeneration_ambiguous", classification.evidence.snapshotDigest);
569
+ if (classification.reason === "incomplete_snapshot" || classification.reason === "out_of_order_snapshot")
570
+ return blockedForState(identity, handle, state, attempts, "incomplete_snapshot", classification.evidence.snapshotDigest);
571
+ if (decision.kind === "continue_owned_turn_observation" || decision.kind === "observe_action_postcondition")
572
+ return undefined;
573
+ if (decision.kind === "continue_preparation")
574
+ return blockedForState(identity, handle, state, attempts, "operation_not_collectable");
575
+ if (decision.kind === "block") {
576
+ const code = decision.code === "target_binding_mismatch" ? "target_binding_mismatch" : decision.code === "target_evidence_unavailable" ? "target_evidence_unavailable" : "operation_state_corrupt";
577
+ return blockedForState(identity, handle, state, attempts, code, classification.evidence.snapshotDigest);
578
+ }
579
+ if (decision.kind === "enter_uncertain") {
580
+ const code = decision.code === "target_evidence_unavailable" ? "target_evidence_unavailable" : decision.code === "capture_ownership_lost" ? "capture_ownership_lost" : "turn_ownership_ambiguous";
581
+ return blockedForState(identity, handle, state, attempts, code, classification.evidence.snapshotDigest);
582
+ }
583
+ return undefined;
584
+ }
585
+ function terminalReceiptFromObservation(identity, durable, classification, terminal, completedAtMs) {
586
+ const cursor = classification.cursor;
587
+ const user = classification.evidence.userTurn;
588
+ const assistant = classification.evidence.assistantTurn;
589
+ if (cursor === undefined || user === undefined || assistant === undefined || cursor.assistantTurnId === undefined || cursor.assistantBranchId === undefined) {
590
+ throw new TypeError("Terminal ownership is incomplete.");
591
+ }
592
+ if (!Number.isFinite(completedAtMs))
593
+ throw new TypeError("Terminal completion time is invalid.");
594
+ const receipt = {
595
+ schemaVersion: OPERATION_RECEIPT_SCHEMA_VERSION,
596
+ operationId: identity.operationId,
597
+ requestDigest: identity.requestDigest,
598
+ targetBindingDigest: durable.binding.targetBindingDigest,
599
+ userTurnId: cursor.userTurnId,
600
+ userTurnEvidenceDigest: user.evidenceDigest,
601
+ assistantTurnId: cursor.assistantTurnId,
602
+ ownershipEvidenceDigest: classification.evidence.snapshotDigest,
603
+ finishReason: terminal.finishReason,
604
+ contentAvailable: terminal.text !== undefined,
605
+ ...(terminal.responseFormat === undefined ? {} : { responseFormat: terminal.responseFormat }),
606
+ artifacts: terminal.artifacts.map((artifact) => {
607
+ const contentDigest = artifact.contentDigest?.startsWith("sha256:")
608
+ ? artifact.contentDigest.slice("sha256:".length)
609
+ : undefined;
610
+ return {
611
+ schemaVersion: OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION,
612
+ operationId: identity.operationId,
613
+ artifactKey: `artifact-${artifact.ordinal}`,
614
+ assistantTurnId: cursor.assistantTurnId,
615
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
616
+ kind: artifact.kind,
617
+ ordinal: artifact.ordinal,
618
+ ...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType }),
619
+ ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
620
+ ...(contentDigest === undefined ? {} : { sha256: contentDigest }),
621
+ status: "available"
622
+ };
623
+ }),
624
+ completedAt: new Date(completedAtMs).toISOString()
625
+ };
626
+ if (terminal.text !== undefined) {
627
+ if (terminal.text.bytes === undefined)
628
+ throw new TypeError("Terminal response metadata is missing bytes.");
629
+ receipt.responseDigest = terminal.text.digest;
630
+ receipt.responseBytes = terminal.text.bytes;
631
+ }
632
+ assertReceiptSafe(receipt);
633
+ return receipt;
634
+ }
635
+ function addLiveContent(result, terminal, classification) {
636
+ if (result.kind !== "completed")
637
+ return result;
638
+ const user = classification.evidence.userTurn;
639
+ const assistant = classification.evidence.assistantTurn;
640
+ const cursor = classification.cursor;
641
+ if (user === undefined || assistant === undefined || cursor?.assistantTurnId === undefined || cursor.assistantBranchId === undefined)
642
+ return result;
643
+ const turn = {
644
+ ...result.turn,
645
+ userOrdinal: user.ordinal,
646
+ assistantOrdinal: assistant.ordinal,
647
+ assistantEvidenceDigest: assistant.evidenceDigest,
648
+ branchStableId: cursor.assistantBranchId
649
+ };
650
+ if (terminal.rawText === undefined) {
651
+ return {
652
+ ...result,
653
+ turn,
654
+ response: {
655
+ ...result.response,
656
+ ...(terminal.text === undefined ? {} : { text: terminal.text }),
657
+ ...(terminal.responseFormat === undefined ? {} : { responseFormat: terminal.responseFormat }),
658
+ rawContentAvailable: false
659
+ }
660
+ };
661
+ }
662
+ return {
663
+ ...result,
664
+ turn,
665
+ response: {
666
+ ...result.response,
667
+ ...(terminal.text === undefined ? {} : { text: terminal.text }),
668
+ ...(terminal.responseFormat === undefined ? {} : { responseFormat: terminal.responseFormat }),
669
+ rawContentAvailable: true,
670
+ rawText: terminal.rawText
671
+ }
672
+ };
673
+ }
674
+ /**
675
+ * Validate the only receipt convergence permitted after a live terminal
676
+ * observation. The collector owns the terminal evidence; a persistence
677
+ * implementation may add the result of an explicitly requested local
678
+ * artifact transfer, but it may not rewrite the operation, turn, response,
679
+ * or artifact identity. `completedAt` is intentionally excluded because the
680
+ * journal is allowed to assign its own durable completion timestamp.
681
+ */
682
+ function assertConvergedTerminalReceipt(persisted, observed) {
683
+ assertReceiptSafe(persisted);
684
+ const withoutArtifacts = (receipt) => {
685
+ const { artifacts: _artifacts, completedAt: _completedAt, ...identity } = receipt;
686
+ return identity;
687
+ };
688
+ if (canonicalJson(withoutArtifacts(persisted)) !== canonicalJson(withoutArtifacts(observed))) {
689
+ throw new TypeError("terminal persistence changed non-artifact terminal evidence");
690
+ }
691
+ if (persisted.artifacts.length !== observed.artifacts.length) {
692
+ throw new TypeError("terminal persistence dropped or added an artifact");
693
+ }
694
+ for (let index = 0; index < observed.artifacts.length; index += 1) {
695
+ const expected = observed.artifacts[index];
696
+ const actual = persisted.artifacts[index];
697
+ if (expected === undefined || actual === undefined) {
698
+ throw new TypeError("terminal persistence returned an incomplete artifact list");
699
+ }
700
+ assertConvergedArtifact(actual, expected);
701
+ }
702
+ }
703
+ function assertConvergedArtifact(persisted, observed) {
704
+ // These fields bind the artifact to the exact terminal turn. They are not
705
+ // transfer results and therefore must be byte-for-byte stable.
706
+ const immutableKeys = [
707
+ "schemaVersion",
708
+ "operationId",
709
+ "artifactKey",
710
+ "assistantTurnId",
711
+ "sourceIdentityDigest",
712
+ "kind",
713
+ "ordinal",
714
+ "mimeType"
715
+ ];
716
+ for (const key of immutableKeys) {
717
+ if (canonicalJson(persisted[key]) !== canonicalJson(observed[key])) {
718
+ throw new TypeError(`terminal persistence changed artifact ${key}`);
719
+ }
720
+ }
721
+ // A normal collect receipt is exactly the browser observation. Once a
722
+ // local transfer is requested, only the transfer fields and status may be
723
+ // added/changed, and only in one direction from the initial available
724
+ // state. This rejects reordered artifacts as well: ordinal and source
725
+ // identity are checked at their original array position above.
726
+ if (observed.status !== "available") {
727
+ throw new TypeError("terminal observation artifact must start available");
728
+ }
729
+ if (persisted.status === "available") {
730
+ if (canonicalJson(persisted) !== canonicalJson(observed)) {
731
+ throw new TypeError("terminal persistence changed an available artifact without a transfer result");
732
+ }
733
+ return;
734
+ }
735
+ if (persisted.status !== "transferred" && persisted.status !== "partial" && persisted.status !== "blocked") {
736
+ throw new TypeError("terminal persistence returned an unsupported artifact status");
737
+ }
738
+ // Browser-observed content facts are part of the exact source identity. A
739
+ // transfer may fill facts the provider did not expose, but it may never
740
+ // contradict byte/hash evidence that was already bound to the owned turn.
741
+ if (observed.bytes !== undefined && persisted.bytes !== observed.bytes) {
742
+ throw new TypeError("terminal persistence changed an observed artifact byte count");
743
+ }
744
+ if (observed.sha256 !== undefined && persisted.sha256 !== observed.sha256) {
745
+ throw new TypeError("terminal persistence changed an observed artifact SHA-256");
746
+ }
747
+ // The durable state validator also enforces these shapes. Keep the checks
748
+ // here because this helper is the explicit trust boundary between the
749
+ // persistence return value and the live terminal observation.
750
+ if (persisted.outputKey !== undefined
751
+ && (typeof persisted.outputKey !== "string" || !TRANSFER_OUTPUT_KEY_PATTERN.test(persisted.outputKey))) {
752
+ throw new TypeError("terminal persistence returned an unsafe artifact output key");
753
+ }
754
+ if (persisted.bytes !== undefined && (!Number.isSafeInteger(persisted.bytes) || persisted.bytes < 0)) {
755
+ throw new TypeError("terminal persistence returned invalid artifact byte count");
756
+ }
757
+ if (persisted.sha256 !== undefined && !/^[0-9a-f]{64}$/u.test(persisted.sha256)) {
758
+ throw new TypeError("terminal persistence returned invalid artifact SHA-256");
759
+ }
760
+ if (persisted.blockerCode !== undefined
761
+ && (typeof persisted.blockerCode !== "string" || !BLOCKER_CODE_PATTERN.test(persisted.blockerCode))) {
762
+ throw new TypeError("terminal persistence returned invalid artifact blocker");
763
+ }
764
+ if (persisted.status === "transferred") {
765
+ if (persisted.outputKey === undefined || persisted.bytes === undefined || persisted.sha256 === undefined || persisted.blockerCode !== undefined) {
766
+ throw new TypeError("transferred artifact receipt is incomplete");
767
+ }
768
+ }
769
+ else if (persisted.blockerCode === undefined) {
770
+ throw new TypeError("partial or blocked artifact receipt is missing a blocker");
771
+ }
772
+ }
773
+ function completedFromReceipt(identity, handle, receipt, attempts) {
774
+ if (receipt === undefined || receipt.schemaVersion !== OPERATION_RECEIPT_SCHEMA_VERSION || receipt.operationId !== identity.operationId || receipt.requestDigest !== identity.requestDigest || (handle.targetBindingDigest !== undefined && receipt.targetBindingDigest !== handle.targetBindingDigest)) {
775
+ return blocked(identity, "operation_state_corrupt", attempts, "completed", "send_may_have_occurred", handle.targetBindingDigest);
776
+ }
777
+ try {
778
+ assertReceiptSafe(receipt);
779
+ }
780
+ catch {
781
+ return blocked(identity, "operation_state_corrupt", attempts, "completed", "send_may_have_occurred", handle.targetBindingDigest);
782
+ }
783
+ const artifacts = receipt.artifacts.map(artifact => ({
784
+ kind: artifact.kind,
785
+ ordinal: artifact.ordinal,
786
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
787
+ ...(artifact.sha256 === undefined ? {} : {
788
+ sha256: artifact.sha256,
789
+ contentDigest: `sha256:${artifact.sha256}`
790
+ }),
791
+ ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
792
+ ...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType }),
793
+ status: artifact.status,
794
+ ...(artifact.outputKey === undefined ? {} : { outputKey: artifact.outputKey }),
795
+ ...(artifact.blockerCode === undefined ? {} : { blockerCode: artifact.blockerCode })
796
+ }));
797
+ return {
798
+ kind: "completed",
799
+ operationId: identity.operationId,
800
+ requestDigest: identity.requestDigest,
801
+ targetBindingDigest: receipt.targetBindingDigest,
802
+ attempts,
803
+ turn: {
804
+ userTurnId: receipt.userTurnId,
805
+ assistantTurnId: receipt.assistantTurnId,
806
+ userTurnEvidenceDigest: receipt.userTurnEvidenceDigest,
807
+ ownershipEvidenceDigest: receipt.ownershipEvidenceDigest
808
+ },
809
+ response: {
810
+ contentAvailable: receipt.contentAvailable,
811
+ rawContentAvailable: false,
812
+ ...(receipt.responseFormat === undefined ? {} : { responseFormat: receipt.responseFormat }),
813
+ ...(receipt.responseDigest === undefined ? {} : {
814
+ text: {
815
+ digest: receipt.responseDigest,
816
+ ...(receipt.responseBytes === undefined ? {} : { bytes: receipt.responseBytes })
817
+ }
818
+ }),
819
+ artifacts: Object.freeze(artifacts),
820
+ finishReason: receipt.finishReason
821
+ }
822
+ };
823
+ }
824
+ function assertReceiptSafe(receipt) {
825
+ assertExactKeys(receipt, "operation receipt", ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "userTurnId", "userTurnEvidenceDigest", "assistantTurnId", "ownershipEvidenceDigest", "responseDigest", "responseBytes", "responseFormat", "finishReason", "contentAvailable", "artifacts", "completedAt"], ["schemaVersion", "operationId", "requestDigest", "targetBindingDigest", "userTurnId", "userTurnEvidenceDigest", "assistantTurnId", "ownershipEvidenceDigest", "finishReason", "contentAvailable", "artifacts", "completedAt"]);
826
+ if (receipt.schemaVersion !== OPERATION_RECEIPT_SCHEMA_VERSION)
827
+ throw new TypeError("Unsupported operation receipt schema.");
828
+ assertOperationId(receipt.operationId, "receipt.operationId");
829
+ assertDigest(receipt.requestDigest, "receipt.requestDigest");
830
+ assertId(receipt.userTurnId, "receipt.userTurnId");
831
+ assertId(receipt.assistantTurnId, "receipt.assistantTurnId");
832
+ assertDigest(receipt.userTurnEvidenceDigest, "receipt.userTurnEvidenceDigest");
833
+ assertDigest(receipt.targetBindingDigest, "receipt.targetBindingDigest");
834
+ assertDigest(receipt.ownershipEvidenceDigest, "receipt.ownershipEvidenceDigest");
835
+ if (receipt.responseFormat !== undefined && receipt.responseFormat !== "markdown" && receipt.responseFormat !== "text")
836
+ throw new TypeError("Receipt response format is invalid.");
837
+ if (typeof receipt.contentAvailable !== "boolean")
838
+ throw new TypeError("Receipt contentAvailable is invalid.");
839
+ const hasResponseDigest = receipt.responseDigest !== undefined;
840
+ const hasResponseBytes = receipt.responseBytes !== undefined;
841
+ if (hasResponseDigest !== hasResponseBytes || (receipt.contentAvailable && !hasResponseDigest))
842
+ throw new TypeError("Receipt response metadata is incomplete.");
843
+ if (receipt.responseDigest !== undefined)
844
+ assertDigest(receipt.responseDigest, "receipt.responseDigest");
845
+ if (receipt.responseBytes !== undefined) {
846
+ assertNonNegativeSafeInteger(receipt.responseBytes, "receipt.responseBytes");
847
+ if (receipt.responseBytes > MAX_RESPONSE_BYTES)
848
+ throw new TypeError("Receipt response bytes exceed the bounded limit.");
849
+ }
850
+ if (typeof receipt.finishReason !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(receipt.finishReason))
851
+ throw new TypeError("Receipt finish reason is invalid.");
852
+ if (typeof receipt.completedAt !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(receipt.completedAt) || new Date(receipt.completedAt).toISOString() !== receipt.completedAt)
853
+ throw new TypeError("Receipt completion timestamp is invalid.");
854
+ if (!Array.isArray(receipt.artifacts) || receipt.artifacts.length > MAX_ARTIFACTS)
855
+ throw new TypeError("Receipt artifacts exceed cap.");
856
+ const ordinals = new Set();
857
+ const keys = new Set();
858
+ for (const artifact of receipt.artifacts) {
859
+ assertExactKeys(artifact, "operation artifact receipt", ["schemaVersion", "operationId", "artifactKey", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "outputKey", "mimeType", "bytes", "sha256", "status", "blockerCode"], ["schemaVersion", "operationId", "artifactKey", "assistantTurnId", "sourceIdentityDigest", "kind", "ordinal", "status"]);
860
+ if (artifact.schemaVersion !== OPERATION_ARTIFACT_RECEIPT_SCHEMA_VERSION || artifact.operationId !== receipt.operationId || artifact.assistantTurnId !== receipt.assistantTurnId)
861
+ throw new TypeError("Receipt artifact identity does not match the terminal turn.");
862
+ if (typeof artifact.artifactKey !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(artifact.artifactKey) || keys.has(artifact.artifactKey))
863
+ throw new TypeError("Receipt artifact keys are invalid or duplicated.");
864
+ keys.add(artifact.artifactKey);
865
+ if (artifact.kind !== "file" && artifact.kind !== "image" && artifact.kind !== "other")
866
+ throw new TypeError("Receipt artifact kind is invalid.");
867
+ if (ordinals.has(artifact.ordinal))
868
+ throw new TypeError("Receipt artifact ordinals are not unique.");
869
+ if (!Number.isSafeInteger(artifact.ordinal) || artifact.ordinal < 0 || artifact.ordinal >= MAX_ARTIFACTS)
870
+ throw new TypeError("Receipt artifact ordinal is invalid.");
871
+ ordinals.add(artifact.ordinal);
872
+ if (artifact.outputKey !== undefined && (typeof artifact.outputKey !== "string" || !TRANSFER_OUTPUT_KEY_PATTERN.test(artifact.outputKey)))
873
+ throw new TypeError("Receipt artifact output key is invalid.");
874
+ assertDigest(artifact.sourceIdentityDigest, "receipt artifact source identity");
875
+ if (artifact.bytes !== undefined)
876
+ assertNonNegativeSafeInteger(artifact.bytes, "receipt artifact bytes");
877
+ if (artifact.sha256 !== undefined && !/^[0-9a-f]{64}$/u.test(artifact.sha256))
878
+ throw new TypeError("Receipt artifact content digest is invalid.");
879
+ if (artifact.mimeType !== undefined && (typeof artifact.mimeType !== "string" || artifact.mimeType.length === 0 || artifact.mimeType.length > MAX_MIME_LENGTH || /[\u0000-\u001f\u007f]/u.test(artifact.mimeType)))
880
+ throw new TypeError("Receipt artifact MIME type is invalid.");
881
+ if (artifact.blockerCode !== undefined && (typeof artifact.blockerCode !== "string" || !BLOCKER_CODE_PATTERN.test(artifact.blockerCode)))
882
+ throw new TypeError("Receipt artifact blocker is invalid.");
883
+ if (artifact.status !== "available" && artifact.status !== "transferred" && artifact.status !== "partial" && artifact.status !== "blocked")
884
+ throw new TypeError("Receipt artifact status is invalid.");
885
+ if (artifact.status === "transferred" && (artifact.outputKey === undefined || artifact.bytes === undefined || artifact.sha256 === undefined))
886
+ throw new TypeError("Transferred artifact receipt is incomplete.");
887
+ if ((artifact.status === "partial" || artifact.status === "blocked") && artifact.blockerCode === undefined)
888
+ throw new TypeError("Blocked artifact receipt is missing a blocker.");
889
+ if ((artifact.status === "available" || artifact.status === "transferred") && artifact.blockerCode !== undefined)
890
+ throw new TypeError("Available artifact receipt cannot contain a blocker.");
891
+ if (artifact.status === "available" && artifact.outputKey !== undefined)
892
+ throw new TypeError("Available artifact receipt cannot contain an output key.");
893
+ }
894
+ }
895
+ function blockedForState(identity, handle, state, attempts, code, evidenceDigest) {
896
+ return blocked(identity, code, attempts, state.phase, state.mutationBoundary, handle.targetBindingDigest, evidenceDigest);
897
+ }
898
+ function blocked(identity, code, attempts, phase, mutationBoundary, targetBindingDigest, evidenceDigest) {
899
+ const message = {
900
+ operation_not_found: "No durable operation exists for this operation ID.",
901
+ operation_request_mismatch: "The operation ID is bound to a different immutable request.",
902
+ operation_not_collectable: "Operation has not reached a collectable phase.",
903
+ operation_state_corrupt: "Durable operation state is invalid or inconsistent.",
904
+ target_binding_mismatch: "The observed browser target does not match the operation binding.",
905
+ target_evidence_unavailable: "The exact browser target could not be proven.",
906
+ turn_ownership_ambiguous: "The exact operation-owned turn could not be proven.",
907
+ concurrent_user_turn: "A human or concurrent user turn prevents safe collection.",
908
+ regeneration_ambiguous: "Multiple assistant branches prevent safe collection.",
909
+ incomplete_snapshot: "The observed turn snapshot is incomplete.",
910
+ capture_ownership_lost: "The operation-owned terminal turn is no longer provable.",
911
+ operation_cancelled: "Collection was cancelled before a safe terminal result.",
912
+ operation_timeout: "Collection reached its bounded time or attempt limit.",
913
+ port_protocol_violation: "The observation adapter returned an invalid protocol shape.",
914
+ operation_progress_persistence_failed: "Proven operation progress could not be durably persisted.",
915
+ operation_receipt_persistence_failed: "The terminal receipt could not be durably persisted.",
916
+ operation_receipt_indeterminate: "Terminal receipt persistence has an indeterminate outcome; collection will not retry it.",
917
+ operation_receipt_expired: "The operation receipt is no longer available."
918
+ };
919
+ return {
920
+ kind: "blocked",
921
+ operationId: identity.operationId,
922
+ requestDigest: identity.requestDigest,
923
+ ...(targetBindingDigest === undefined ? {} : { targetBindingDigest }),
924
+ blocker: {
925
+ code,
926
+ operationId: identity.operationId,
927
+ requestDigest: identity.requestDigest,
928
+ phase,
929
+ mutationBoundary,
930
+ attempts,
931
+ message: message[code],
932
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
933
+ }
934
+ };
935
+ }
936
+ function pending(identity, handle, targetBindingDigest, state, attempts) {
937
+ return {
938
+ kind: "pending",
939
+ operationId: identity.operationId,
940
+ requestDigest: identity.requestDigest,
941
+ targetBindingDigest,
942
+ phase: state.phase,
943
+ mutationBoundary: state.mutationBoundary,
944
+ attempts
945
+ };
946
+ }
947
+ function isCollectableState(state) {
948
+ if (state.phase === "submitted" || state.phase === "generating" || state.phase === "capturing" || state.phase === "uncertain") {
949
+ return true;
950
+ }
951
+ // A process can die after durably recording Send intent but before its
952
+ // phase event. Collection is the observation-only recovery path for both
953
+ // sides of that crash gap; it never receives a mutation port.
954
+ return (state.phase === "ready" || state.phase === "send_pending")
955
+ && Object.values(state.actions).some(action => action.kind === "send");
956
+ }
957
+ function progressNeedsPersistence(current, desired) {
958
+ return !progressPhaseReached(current, desired);
959
+ }
960
+ function progressPhaseReached(current, desired) {
961
+ if (desired === "submitted") {
962
+ return current === "submitted" || current === "generating" || current === "capturing" || current === "completed";
963
+ }
964
+ return current === "generating" || current === "capturing" || current === "completed";
965
+ }
966
+ function safeHandleIdentity(handle) {
967
+ const candidate = isRecord(handle) ? handle : {};
968
+ return {
969
+ operationId: typeof candidate.operationId === "string" && UUID_PATTERN.test(candidate.operationId)
970
+ ? candidate.operationId
971
+ : "invalid-operation",
972
+ requestDigest: typeof candidate.requestDigest === "string" && HMAC_DIGEST_PATTERN.test(candidate.requestDigest)
973
+ ? candidate.requestDigest
974
+ : "invalid-request"
975
+ };
976
+ }
977
+ function assertExactKeys(value, label, allowed, required) {
978
+ if (!isRecord(value))
979
+ throw new TypeError(`${label} must be an object.`);
980
+ const keys = Object.keys(value);
981
+ const allowedSet = new Set(allowed);
982
+ for (const key of keys)
983
+ if (!allowedSet.has(key))
984
+ throw new TypeError(`${label} contains an unsupported field.`);
985
+ for (const key of required)
986
+ if (!Object.prototype.hasOwnProperty.call(value, key))
987
+ throw new TypeError(`${label} is missing a required field.`);
988
+ }
989
+ function assertId(value, label) {
990
+ if (typeof value !== "string" || !ID_PATTERN.test(value))
991
+ throw new TypeError(`${label} is invalid.`);
992
+ }
993
+ function assertOperationId(value, label) {
994
+ if (typeof value !== "string" || !UUID_PATTERN.test(value))
995
+ throw new TypeError(`${label} is invalid.`);
996
+ }
997
+ function assertDigest(value, label) {
998
+ if (typeof value !== "string" || !HMAC_DIGEST_PATTERN.test(value))
999
+ throw new TypeError(`${label} is invalid.`);
1000
+ }
1001
+ function assertContentDigest(value, label) {
1002
+ if (typeof value !== "string" || !CONTENT_DIGEST_PATTERN.test(value))
1003
+ throw new TypeError(`${label} is invalid.`);
1004
+ }
1005
+ function collectorDurableErrorCode(error) {
1006
+ if (!isRecord(error) || typeof error.code !== "string")
1007
+ return undefined;
1008
+ if (error.code === "operation_not_found")
1009
+ return "operation_not_found";
1010
+ if (error.code === "operation_request_mismatch")
1011
+ return "operation_request_mismatch";
1012
+ if (error.code === "operation_receipt_expired")
1013
+ return "operation_receipt_expired";
1014
+ if (error.code === "journal_corrupt" || error.code === "operation_state_corrupt")
1015
+ return "operation_state_corrupt";
1016
+ return undefined;
1017
+ }
1018
+ function collectorPersistenceErrorCode(error) {
1019
+ if (isRecord(error) && error.code === "operation_receipt_persistence_failed")
1020
+ return "operation_receipt_persistence_failed";
1021
+ return "operation_receipt_indeterminate";
1022
+ }
1023
+ function assertOrdinal(value, label) {
1024
+ if (!Number.isSafeInteger(value) || value < 0 || value > MAX_ARTIFACTS * MAX_ATTEMPTS)
1025
+ throw new TypeError(`${label} is invalid.`);
1026
+ }
1027
+ function assertNonNegativeSafeInteger(value, label) {
1028
+ if (!Number.isSafeInteger(value) || value < 0)
1029
+ throw new TypeError(`${label} is invalid.`);
1030
+ }
1031
+ function isRecord(value) {
1032
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1033
+ }
1034
+ function isAbortSignal(value) {
1035
+ return isRecord(value)
1036
+ && typeof value.aborted === "boolean"
1037
+ && typeof value.addEventListener === "function"
1038
+ && typeof value.removeEventListener === "function";
1039
+ }