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,2810 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { canonicalJson } from "./canonical.js";
3
+ import { OperationJournalError } from "./journal.js";
4
+ import { collectOperation } from "./collector.js";
5
+ import { runAtomicSubmission } from "./submission.js";
6
+ import { runOperationControl, CONTROL_COORDINATOR_SCHEMA_VERSION, controlSteerPreparedDigestMaterial } from "./control.js";
7
+ import { runOperationStaging } from "./staging.js";
8
+ import { assertOperationStateShape, assertOwnershipBaselineShape, durableCapturePolicyFromRequest } from "./state-machine.js";
9
+ import { OPERATION_CONTROL_RECEIPT_SCHEMA_VERSION, OPERATION_HANDLE_SCHEMA_VERSION, OPERATION_RECEIPT_SCHEMA_VERSION, OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION, OPERATION_ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION, OPERATION_ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION, OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION } from "./types.js";
10
+ /**
11
+ * Journal-owning composition for one transactional browser operation.
12
+ *
13
+ * The adapter types in this module are intentionally narrow. The service
14
+ * receives no DOM objects and no browser/page object; the supplied ports only
15
+ * exchange already-normalized observations. The request prompt, local file
16
+ * paths, and raw response text can therefore remain in the caller/adapter
17
+ * closure while the service owns all durable event writes.
18
+ */
19
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
20
+ 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}$/;
21
+ const MAX_CAS_RETRIES = 12;
22
+ const UNCERTAIN_SUBMISSION_BLOCKERS = new Set([
23
+ "ambiguous_file_handoff",
24
+ "ambiguous_submit",
25
+ "journal_unavailable",
26
+ "operation_cancelled",
27
+ "operation_timeout"
28
+ ]);
29
+ /**
30
+ * Messages that may cross the service boundary for failures raised by the
31
+ * journal or by a native provider/filesystem seam. Error objects are often
32
+ * populated with paths, URLs, account identifiers, or private provider
33
+ * diagnostics; retaining their message here would turn a safe operation
34
+ * result into an accidental disclosure channel. Keep this table closed and
35
+ * static: callers can use `code` for machine routing and never need the
36
+ * implementation detail that caused it.
37
+ */
38
+ const SAFE_SERVICE_ERROR_MESSAGES = Object.freeze({
39
+ invalid_operation_handle: "Operation handle could not be validated.",
40
+ operation_not_found: "No durable operation exists for the supplied identity.",
41
+ operation_request_mismatch: "Operation request identity does not match durable state.",
42
+ operation_receipt_expired: "The operation receipt has expired.",
43
+ operation_compacted: "The completed operation is immutable.",
44
+ operation_not_compactable: "Only a completed operation can be compacted.",
45
+ journal_purge_ack_required: "Tombstone purge requires explicit acknowledgement.",
46
+ revision_conflict: "The operation changed concurrently; retry from a fresh handle.",
47
+ invalid_expected_revision: "The expected operation revision is invalid.",
48
+ creation_event_required: "The operation journal requires a creation event first.",
49
+ duplicate_operation_created: "The operation journal already contains its creation event.",
50
+ durable_record_conflict: "A durable operation record already exists.",
51
+ event_not_serializable: "The operation event is not serializable.",
52
+ journal_quota_exceeded: "The operation journal quota is exhausted.",
53
+ journal_unavailable: "The operation journal is unavailable.",
54
+ journal_corrupt: "The operation journal failed integrity validation.",
55
+ journal_snapshot_corrupt: "The operation snapshot failed integrity validation.",
56
+ journal_tombstone_corrupt: "The operation tombstone failed integrity validation.",
57
+ journal_terminal_corrupt: "The operation terminal record failed integrity validation.",
58
+ unsafe_journal_entry: "The operation journal contains an unsafe entry.",
59
+ unsafe_journal_key: "The operation journal key is unsafe.",
60
+ unsafe_state_root: "The operation state root is unsafe.",
61
+ unsafe_state_owner: "The operation state root has an unsafe owner.",
62
+ unsafe_state_permissions: "The operation state root has unsafe permissions.",
63
+ state_root_not_absolute: "The operation state root must be absolute.",
64
+ state_path_escape: "The operation state path is unsafe.",
65
+ journal_lock_timeout: "Timed out waiting for the operation journal lock.",
66
+ journal_lock_corrupt: "The operation journal lock is invalid.",
67
+ journal_lock_changed: "The operation journal lock changed unexpectedly.",
68
+ journal_lock_lost: "The operation journal lock was lost.",
69
+ journal_lock_recovery_abandoned: "The operation journal lock recovery requires manual diagnosis.",
70
+ journal_lock_cleanup_failed: "The operation journal lock could not be cleaned up safely.",
71
+ journal_scan_limit: "The operation journal could not be bounded safely.",
72
+ journal_log_too_large: "The operation log reached its hard safety limit.",
73
+ journal_temp_conflict: "A temporary operation state record already exists.",
74
+ operation_binding_mismatch: "The operation journal identity is inconsistent.",
75
+ invalid_operation_event: "The operation event is invalid.",
76
+ invalid_evidence_domain: "The operation evidence domain is invalid.",
77
+ invalid_evidence_material: "The operation evidence is invalid.",
78
+ evidence_material_too_large: "The operation evidence exceeds its safety limit.",
79
+ invalid_journal_option: "The operation journal options are invalid.",
80
+ invalid_journal_clock: "The operation journal clock is invalid.",
81
+ invalid_journal_entropy: "The operation journal entropy is invalid.",
82
+ invalid_journal_key: "The operation journal key is invalid.",
83
+ journal_key_missing: "The operation journal key is missing.",
84
+ journal_key_missing_with_state: "The operation journal key is missing while durable state exists.",
85
+ default: "The operation failed safely."
86
+ });
87
+ export class OperationServiceError extends Error {
88
+ code;
89
+ constructor(code, message) {
90
+ super(message);
91
+ this.code = code;
92
+ this.name = "OperationServiceError";
93
+ }
94
+ }
95
+ /**
96
+ * Compose the additive operations surface over an authenticated journal.
97
+ * Construct one instance per stable state root and share it between callers.
98
+ */
99
+ export class OperationService {
100
+ journal;
101
+ now;
102
+ maxCasRetries;
103
+ /**
104
+ * Same-process convergence for a collector race. The journal remains the
105
+ * cross-process authority; this map prevents two callers from even asking
106
+ * an arbitrary adapter to begin the same local effect concurrently.
107
+ */
108
+ artifactTransfersInFlight = new Map();
109
+ constructor(journal, options = {}) {
110
+ this.journal = journal;
111
+ this.now = options.now ?? Date.now;
112
+ this.maxCasRetries = options.maxCasRetries ?? MAX_CAS_RETRIES;
113
+ if (!Number.isSafeInteger(this.maxCasRetries) || this.maxCasRetries < 1 || this.maxCasRetries > 100) {
114
+ throw new OperationServiceError("invalid_cas_retries", "maxCasRetries must be a positive bounded integer.");
115
+ }
116
+ }
117
+ /**
118
+ * Submit once and return a fresh locator. A successful return means only
119
+ * that the submission result has been reconciled and durably bridged; it
120
+ * does not wait for assistant generation.
121
+ */
122
+ async submit(request, files, adapter, options = {}) {
123
+ const requestDigest = this.computeRequestDigest(request, files, options.requestDigest);
124
+ const signal = options.signal ?? new AbortController().signal;
125
+ if (!isAbortSignal(signal))
126
+ throw new OperationServiceError("invalid_signal", "Submission signal must be an AbortSignal.");
127
+ if (signal.aborted)
128
+ throw new OperationServiceError("operation_cancelled", "The operation was cancelled before submission.");
129
+ let loaded = await this.ensureCreated(request, requestDigest);
130
+ if (loaded.state.phase === "completed" && loaded.state.receipt !== undefined) {
131
+ return {
132
+ handle: this.journal.handleFromState(loaded.state),
133
+ submission: submissionFromCompleted(loaded.state)
134
+ };
135
+ }
136
+ let resolution;
137
+ try {
138
+ resolution = await this.resolveAndBindTarget(request, requestDigest, adapter, signal, loaded);
139
+ }
140
+ catch (error) {
141
+ // Creation is already durable at this point. A read-only target probe
142
+ // failure must therefore return the same resumable operation identity,
143
+ // not collapse into a transport error that loses the handle.
144
+ const current = await this.journal.load(request.operationId, requestDigest);
145
+ const handle = this.journal.handleFromState(current.state);
146
+ await this.persistReturnedSubmissionBlocker(submissionFromTargetResolutionFailure(current.state, handle, error, signal));
147
+ const fresh = await this.journal.load(request.operationId, requestDigest);
148
+ const freshHandle = this.journal.handleFromState(fresh.state);
149
+ return {
150
+ handle: freshHandle,
151
+ submission: submissionFromTargetResolutionFailure(fresh.state, freshHandle, error, signal)
152
+ };
153
+ }
154
+ loaded = resolution.loaded;
155
+ const targetBindingDigest = resolution.targetBindingDigest;
156
+ if (loaded.state.phase === "prepared" && adapter.staging !== undefined) {
157
+ const staging = await this.stageRequest(request, requestDigest, targetBindingDigest, adapter.staging, signal, options.deadlineAt);
158
+ if (staging !== undefined) {
159
+ await this.persistReturnedSubmissionBlocker(staging);
160
+ const fresh = await this.journal.load(request.operationId, requestDigest);
161
+ return { handle: this.journal.handleFromState(fresh.state), submission: staging };
162
+ }
163
+ loaded = await this.journal.load(request.operationId, requestDigest);
164
+ }
165
+ const attachmentManifest = files.map((file, ordinal) => ({
166
+ identityDigest: this.journal.evidenceDigest("file-manifest", { ordinal, ...file }),
167
+ ordinal
168
+ }));
169
+ const expected = {
170
+ surface: request.surface,
171
+ targetBindingDigest,
172
+ configurationReceiptDigest: resolution.configurationReceiptDigest,
173
+ composerReceiptDigest: resolution.composerReceiptDigest,
174
+ attachmentManifest: {
175
+ count: attachmentManifest.length,
176
+ orderPolicy: "exact",
177
+ identities: attachmentManifest
178
+ }
179
+ };
180
+ const send = uniqueAction(loaded.state, "send");
181
+ const handoff = uniqueAction(loaded.state, "file_handoff");
182
+ const operation = {
183
+ state: loaded.state,
184
+ handle: this.journal.handleFromState(loaded.state),
185
+ actionIds: {
186
+ sendActionId: send?.actionId ?? randomUUID(),
187
+ ...(files.length === 0 ? {} : { fileHandoffActionId: handoff?.actionId ?? randomUUID() })
188
+ }
189
+ };
190
+ const submission = await runAtomicSubmission(operation, expected, this.submissionPorts(adapter.submission), { signal, ...(options.deadlineAt === undefined ? {} : { deadlineAt: options.deadlineAt }) });
191
+ await this.persistReturnedSubmissionBlocker(submission);
192
+ const fresh = await this.journal.load(request.operationId, requestDigest);
193
+ return { handle: this.journal.handleFromState(fresh.state), submission };
194
+ }
195
+ /**
196
+ * Collect from a caller locator. It reloads the journal for every collector
197
+ * attempt and never calls the submission path. Completed receipts are
198
+ * returned directly, so a browser adapter is not needed for that case.
199
+ */
200
+ async collect(handle, adapter, options = {}) {
201
+ const loaded = await this.loadForHandle(handle);
202
+ if (loaded.state.phase === "completed" && loaded.state.receipt !== undefined) {
203
+ return completedFromState(handle, loaded.state);
204
+ }
205
+ if (loaded.state.target === undefined) {
206
+ throw new OperationServiceError("target_binding_missing", "Collect requires a durable target binding.");
207
+ }
208
+ if (loaded.state.target.targetLifecycle === "new_pending") {
209
+ throw new OperationServiceError("target_establishment_required", "Collect requires durable provider identity establishment for a new target.");
210
+ }
211
+ const signal = options.signal ?? new AbortController().signal;
212
+ const ports = {
213
+ readDurable: async ({ handle: requestedHandle }) => {
214
+ const current = await this.loadForHandle(requestedHandle);
215
+ if (current.state.target === undefined)
216
+ throw new OperationJournalError("operation_state_inconsistent", "Operation target binding is missing.");
217
+ const ownership = latestSubmissionOwnership(current.state);
218
+ if (ownership === undefined) {
219
+ throw new OperationServiceError("submission_witness_missing", "Collect cannot recover ownership without a durable causal baseline and witness.");
220
+ }
221
+ const targetBindingDigest = this.targetBindingDigest(current.state);
222
+ const projectedWitness = ownershipWitnessFromDurable(ownership.witness);
223
+ const context = await adapter.collector.readContext({
224
+ operationId: current.state.operationId,
225
+ requestDigest: current.state.requestDigest,
226
+ targetBindingDigest,
227
+ submissionActionId: ownership.action.actionId,
228
+ submissionActionKind: ownership.action.kind,
229
+ submissionWitness: projectedWitness,
230
+ baseline: ownership.baseline.baseline,
231
+ signal
232
+ });
233
+ // The browser adapter may return a candidate observation for the
234
+ // current process, but it is never allowed to recreate durable
235
+ // ownership authority after restart. Project only the authenticated
236
+ // witness from journal state into the collect-only context.
237
+ return {
238
+ state: current.state,
239
+ // Keep browser-derived target evidence but project the causal
240
+ // action identity from authenticated state. A later Work steer
241
+ // owns its own delta; a legacy Send default cannot reclassify it.
242
+ binding: {
243
+ ...context.binding,
244
+ operationId: current.state.operationId,
245
+ targetBindingDigest,
246
+ actionId: ownership.action.actionId,
247
+ actionKind: ownership.action.kind
248
+ },
249
+ // Never trust the adapter's current snapshot as the pre-Send
250
+ // baseline. The authenticated journal is the sole restart authority.
251
+ baseline: ownership.baseline.baseline,
252
+ submissionWitness: projectedWitness,
253
+ // A cursor captured under the original Send cannot be reused after
254
+ // a Work steer: its assistant branch/delta would classify the
255
+ // steer-owned user turn as foreign concurrent activity. The Work
256
+ // baseline+witness pair is the sole post-steer recovery anchor.
257
+ ...(ownership.action.kind === "work_steer" || context.prior === undefined ? {} : { prior: context.prior })
258
+ };
259
+ },
260
+ observe: request => adapter.collector.observe(request),
261
+ sleep: (milliseconds, sleepSignal) => adapter.collector.sleep(milliseconds, sleepSignal),
262
+ persistProgress: request => this.persistProgress(request),
263
+ persistTerminal: request => this.persistTerminal(request, adapter.artifacts)
264
+ };
265
+ // New records carry the immutable capture contract. Fill omitted
266
+ // collect knobs from that contract so a restart-safe collect call does
267
+ // not accidentally fall back to the historical include/Markdown defaults.
268
+ const capturePolicy = loaded.state.capturePolicy;
269
+ const policyDefaults = capturePolicy === undefined
270
+ ? {}
271
+ : {
272
+ ...(options.responseContent === undefined ? { responseContent: capturePolicy.responseContent } : {}),
273
+ ...(options.responseFormat === undefined ? { responseFormat: capturePolicy.responseFormat } : {})
274
+ };
275
+ const effectiveOptions = { ...policyDefaults, ...options };
276
+ return await collectOperation(handle, ports, effectiveOptions);
277
+ }
278
+ /** Browser-free state inspection. The adapter is intentionally not accepted. */
279
+ async inspect(handle) {
280
+ const loaded = await this.loadForHandle(handle);
281
+ return { state: loaded.state, handle: this.journal.handleFromState(loaded.state) };
282
+ }
283
+ /**
284
+ * Persist the one-way provider identity refinement for a genuine new
285
+ * target. This seam is browser-free: the adapter must first prove the exact
286
+ * post-Send user turn and provider identity, then call this method. It never
287
+ * allocates or guesses a conversation ID and it converges identical
288
+ * concurrent observations without appending a duplicate event.
289
+ */
290
+ async establishTarget(request) {
291
+ request = validateTargetEstablishmentRequest(request);
292
+ if (request.postSendDeltaDigest === undefined) {
293
+ throw new OperationServiceError("target_establishment_delta_missing", "New-target establishment requires exact post-Send delta evidence.");
294
+ }
295
+ let loaded = await this.journal.load(request.operationId, request.requestDigest);
296
+ const currentHandle = this.journal.handleFromState(loaded.state);
297
+ if (currentHandle.targetBindingDigest !== request.targetBindingDigest) {
298
+ throw new OperationServiceError("target_binding_mismatch", "Target establishment target digest does not match durable state.");
299
+ }
300
+ const target = loaded.state.target;
301
+ if (target === undefined) {
302
+ throw new OperationServiceError("target_binding_missing", "Target establishment requires a durable target anchor.");
303
+ }
304
+ if ((target.targetLifecycle ?? "fixed") === "fixed") {
305
+ throw new OperationServiceError("fixed_target_establishment", "A fixed target cannot be established as a new conversation.");
306
+ }
307
+ const send = loaded.state.actions[request.causalSendActionId];
308
+ if (send === undefined || send.kind !== "send") {
309
+ throw new OperationServiceError("target_establishment_send_missing", "Target establishment requires the causal original Send intent.");
310
+ }
311
+ if (send.outcome === "not_satisfied") {
312
+ throw new OperationServiceError("target_establishment_send_rejected", "Target establishment cannot follow a rejected Send intent.");
313
+ }
314
+ if (send.targetDigest !== request.targetBindingDigest) {
315
+ throw new OperationServiceError("target_establishment_target_mismatch", "Target establishment target digest does not match the causal Send intent.");
316
+ }
317
+ if (loaded.state.phase === "prepared" || loaded.state.phase === "handoff_pending" || loaded.state.phase === "completed") {
318
+ throw new OperationServiceError("target_establishment_phase_invalid", "Target establishment requires a durable Send lifecycle phase.");
319
+ }
320
+ const observedAt = request.observedAt ?? this.timestamp(loaded.state.updatedAt);
321
+ if (observedAt < send.intentAt) {
322
+ throw new OperationServiceError("target_establishment_before_send", "Target establishment cannot precede the durable Send intent.");
323
+ }
324
+ const establishment = {
325
+ targetBindingDigest: request.targetBindingDigest,
326
+ anchorDigest: request.anchorDigest,
327
+ causalSendActionId: request.causalSendActionId,
328
+ conversationId: request.conversationId,
329
+ canonicalThreadUrl: request.canonicalThreadUrl,
330
+ userTurnId: request.userTurnId,
331
+ userTurnEvidenceDigest: request.userTurnEvidenceDigest,
332
+ postSendDeltaDigest: request.postSendDeltaDigest,
333
+ evidenceDigest: request.evidenceDigest,
334
+ observedAt
335
+ };
336
+ const sameEstablishment = (state) => {
337
+ const existing = state.target?.targetEstablishment;
338
+ return state.target?.targetLifecycle === "new_established"
339
+ && existing !== undefined
340
+ && canonicalJson(existing) === canonicalJson(establishment);
341
+ };
342
+ if (sameEstablishment(loaded.state)) {
343
+ if (loaded.state.submissionWitness !== undefined) {
344
+ if (loaded.state.ownershipBaseline === undefined
345
+ || !submissionWitnessMatchesEstablishment(loaded.state.submissionWitness, establishment, loaded.state.ownershipBaseline.baseline.snapshotDigest)) {
346
+ throw new OperationServiceError("submission_witness_conflict", "A durable submission witness conflicts with the established target evidence.");
347
+ }
348
+ return { state: loaded.state, handle: this.journal.handleFromState(loaded.state) };
349
+ }
350
+ const withWitness = await this.appendSubmissionWitnessConvergent(request.operationId, request.requestDigest, this.submissionWitnessFromEstablishment(establishment, loaded.state.ownershipBaseline?.baseline.snapshotDigest, loaded.state.updatedAt));
351
+ return { state: withWitness.state, handle: this.journal.handleFromState(withWitness.state) };
352
+ }
353
+ if (loaded.state.target?.targetLifecycle === "new_established") {
354
+ throw new OperationServiceError("target_establishment_conflict", "A different provider identity is already durably established.");
355
+ }
356
+ const event = {
357
+ type: "target_established",
358
+ establishment
359
+ };
360
+ try {
361
+ loaded = await this.appendConvergent(request.operationId, request.requestDigest, event, sameEstablishment);
362
+ }
363
+ catch (error) {
364
+ // A concurrent writer may have established a different provider
365
+ // identity between our read and append. Surface that as an explicit
366
+ // fail-closed conflict instead of leaking a reducer/journal error.
367
+ try {
368
+ const observed = await this.journal.load(request.operationId, request.requestDigest);
369
+ if (observed.state.target?.targetLifecycle === "new_established") {
370
+ if (sameEstablishment(observed.state)) {
371
+ loaded = observed;
372
+ }
373
+ else {
374
+ throw new OperationServiceError("target_establishment_conflict", "A different provider identity is already durably established.");
375
+ }
376
+ }
377
+ else {
378
+ throw error;
379
+ }
380
+ }
381
+ catch (reconcileError) {
382
+ if (reconcileError instanceof OperationServiceError)
383
+ throw reconcileError;
384
+ throw error;
385
+ }
386
+ }
387
+ const withWitness = await this.appendSubmissionWitnessConvergent(request.operationId, request.requestDigest, this.submissionWitnessFromEstablishment(establishment, loaded.state.ownershipBaseline?.baseline.snapshotDigest, loaded.state.updatedAt));
388
+ const fresh = await this.journal.load(request.operationId, request.requestDigest);
389
+ if (!sameEstablishment(fresh.state) || fresh.state.submissionWitness === undefined || withWitness.state.submissionWitness === undefined) {
390
+ throw new OperationServiceError("target_establishment_indeterminate", "Target establishment was not durably validated after persistence.");
391
+ }
392
+ return { state: fresh.state, handle: this.journal.handleFromState(fresh.state) };
393
+ }
394
+ /** Submit followed by collect with the same operation ID and handle. */
395
+ async run(request, files, adapter, options = {}) {
396
+ const submit = await this.submit(request, files, adapter, options);
397
+ if (submit.submission.kind !== "submitted"
398
+ && submit.submission.kind !== "already_submitted"
399
+ && submit.submission.kind !== "completed_receipt") {
400
+ return { submit };
401
+ }
402
+ const collected = await this.collect(submit.handle, adapter, options);
403
+ return { submit, collect: collected };
404
+ }
405
+ /** Run one operation-bound Stop or Work steer through the same journal. */
406
+ async control(request, adapter, options = {}) {
407
+ if (adapter.control === undefined) {
408
+ throw new OperationServiceError("control_unavailable", "The operation adapter does not expose control ports.");
409
+ }
410
+ const requestDigest = this.journal.controlRequestDigest(request);
411
+ const ports = {
412
+ readParent: requestForParent => this.readControlParent(requestForParent),
413
+ observeTurn: requestForTurn => adapter.control.observeTurn(requestForTurn),
414
+ persistActionIntent: action => this.persistControlIntent(action),
415
+ executeOnce: execution => adapter.control.executeOnce(execution),
416
+ observePostcondition: postcondition => adapter.control.observePostcondition(postcondition),
417
+ ...(adapter.control.postconditionRetry === undefined
418
+ ? {}
419
+ : { postconditionRetry: adapter.control.postconditionRetry }),
420
+ persistReceipt: receipt => this.persistControlReceipt(receipt)
421
+ };
422
+ if (typeof adapter.control.prepareSteer === "function") {
423
+ ports.prepareSteer = requestForPrepare => adapter.control.prepareSteer(requestForPrepare);
424
+ }
425
+ if (typeof adapter.control.executeSteerPrepared === "function") {
426
+ ports.executeSteerPrepared = requestForExecute => adapter.control.executeSteerPrepared(requestForExecute);
427
+ }
428
+ if (typeof adapter.control.verifySteer === "function") {
429
+ ports.verifySteer = requestForVerify => adapter.control.verifySteer(requestForVerify);
430
+ }
431
+ if (typeof adapter.control.recoverSteer === "function") {
432
+ ports.recoverSteer = requestForRecover => adapter.control.recoverSteer(requestForRecover);
433
+ }
434
+ ports.persistSteerIntentAndBaseline = requestForPersistence => this.persistSteerIntentAndBaseline(requestForPersistence);
435
+ return await runOperationControl(request, requestDigest, ports, options);
436
+ }
437
+ computeRequestDigest(request, files, provided) {
438
+ let computed;
439
+ try {
440
+ computed = this.journal.submitRequestDigest(request, files);
441
+ }
442
+ catch {
443
+ // Request canonicalization operates on caller-controlled input. Never
444
+ // forward native messages (or invoke message/string accessors) across
445
+ // the public operations boundary.
446
+ throw new OperationServiceError("invalid_operation_request", "The immutable operation request could not be canonicalized.");
447
+ }
448
+ if (provided !== undefined) {
449
+ if (!DIGEST_PATTERN.test(provided))
450
+ throw new OperationServiceError("invalid_request_digest", "requestDigest is not canonical.");
451
+ if (provided !== computed)
452
+ throw new OperationServiceError("operation_request_mismatch", "Provided requestDigest does not match the immutable request.");
453
+ }
454
+ return computed;
455
+ }
456
+ async ensureCreated(request, requestDigest) {
457
+ const createdAt = this.timestamp();
458
+ const event = {
459
+ type: "operation_created",
460
+ operationId: request.operationId,
461
+ requestDigest,
462
+ surface: request.surface,
463
+ createdAt,
464
+ capturePolicy: durableCapturePolicyFromRequest(request.capture)
465
+ };
466
+ try {
467
+ return await this.journal.create(event);
468
+ }
469
+ catch (error) {
470
+ // A fault injector/process crash can occur after the creation record is
471
+ // durable but before append() returns. Reconcile that prefix safely.
472
+ try {
473
+ return await this.journal.load(request.operationId, requestDigest);
474
+ }
475
+ catch {
476
+ throw this.serviceError(error, "journal_unavailable");
477
+ }
478
+ }
479
+ }
480
+ async resolveAndBindTarget(request, requestDigest, adapter, signal, loaded) {
481
+ if (!adapter || typeof adapter.resolveTarget !== "function" || !adapter.submission || !adapter.collector) {
482
+ throw new OperationServiceError("adapter_incomplete", "Operation adapter is incomplete.");
483
+ }
484
+ const durableSubmit = Object.values(loaded.state.actions).some(action => action.kind === "send");
485
+ // Once a non-repeatable Send/steer intent is durable, retrying submit is
486
+ // observation-only. Do not make recovery depend on a fresh target probe
487
+ // or staging read that could itself be unavailable; the immutable target
488
+ // already recorded in the journal is the only target authority.
489
+ if (durableSubmit && loaded.state.target !== undefined) {
490
+ const targetBindingDigest = this.targetBindingDigest(loaded.state);
491
+ const configurationReceiptDigest = loaded.state.target.configurationReceiptDigest
492
+ ?? this.journal.evidenceDigest("configuration-request", requestDigest);
493
+ const composerReceiptDigest = this.journal.evidenceDigest("composer-request", requestDigest);
494
+ return { loaded, targetBindingDigest, configurationReceiptDigest, composerReceiptDigest };
495
+ }
496
+ let resolution;
497
+ try {
498
+ resolution = await adapter.resolveTarget({
499
+ operationId: request.operationId,
500
+ requestDigest,
501
+ surface: request.surface,
502
+ target: request.target,
503
+ signal
504
+ });
505
+ }
506
+ catch (error) {
507
+ // Preserve only the thrown value for the outer redacted classifier.
508
+ // Reading an adapter Error.message here could invoke a hostile accessor
509
+ // or retain provider-private request/account details.
510
+ throw error;
511
+ }
512
+ validateTargetResolution(resolution);
513
+ const expectedConfigurationReceiptDigest = this.journal.evidenceDigest("configuration-request", requestDigest);
514
+ const expectedComposerReceiptDigest = this.journal.evidenceDigest("composer-request", requestDigest);
515
+ if (resolution.configurationReceiptDigest !== undefined
516
+ && resolution.configurationReceiptDigest !== expectedConfigurationReceiptDigest) {
517
+ throw new OperationServiceError("configuration_drift", "Target resolution returned a configuration receipt outside the operation identity domain.");
518
+ }
519
+ if (resolution.composerReceiptDigest !== undefined
520
+ && resolution.composerReceiptDigest !== expectedComposerReceiptDigest) {
521
+ throw new OperationServiceError("composer_drift", "Target resolution returned a composer receipt outside the operation identity domain.");
522
+ }
523
+ if (resolution.target.configurationReceiptDigest !== undefined
524
+ && resolution.target.configurationReceiptDigest !== expectedConfigurationReceiptDigest) {
525
+ throw new OperationServiceError("configuration_drift", "Resolved target configuration receipt disagrees with the immutable request.");
526
+ }
527
+ const resolvedTarget = {
528
+ ...resolution.target,
529
+ configurationReceiptDigest: expectedConfigurationReceiptDigest
530
+ };
531
+ let current = loaded;
532
+ if (current.state.target === undefined) {
533
+ current = await this.appendTarget(current, resolvedTarget);
534
+ }
535
+ else if (canonicalJson(current.state.target) !== canonicalJson(resolvedTarget)) {
536
+ throw new OperationServiceError("target_binding_mismatch", "The durable operation target binding is immutable.");
537
+ }
538
+ const targetBindingDigest = this.targetBindingDigest(current.state);
539
+ const configurationReceiptDigest = current.state.target?.configurationReceiptDigest
540
+ ?? expectedConfigurationReceiptDigest;
541
+ const composerReceiptDigest = expectedComposerReceiptDigest;
542
+ assertDigest(configurationReceiptDigest, "configurationReceiptDigest");
543
+ assertDigest(composerReceiptDigest, "composerReceiptDigest");
544
+ return { loaded: current, targetBindingDigest, configurationReceiptDigest, composerReceiptDigest };
545
+ }
546
+ async appendTarget(loaded, target) {
547
+ const event = {
548
+ type: "target_bound",
549
+ target,
550
+ observedAt: this.timestamp(loaded.state.updatedAt)
551
+ };
552
+ return this.appendConvergent(loaded.state.operationId, loaded.state.requestDigest, event, state => state.target !== undefined && canonicalJson(state.target) === canonicalJson(target));
553
+ }
554
+ submissionPorts(adapter) {
555
+ return {
556
+ observeStaging: (request) => adapter.observeStaging(request),
557
+ executeFileHandoffOnce: (request) => adapter.executeFileHandoffOnce(request),
558
+ observeAttachments: (request) => adapter.observeAttachments(request),
559
+ prepareSend: (request) => adapter.prepareSend(request),
560
+ persistPreparedSend: (request) => this.persistPreparedSend(request),
561
+ executePreparedSend: (request) => adapter.executePreparedSend(request),
562
+ verifyPreparedSend: (request) => adapter.verifyPreparedSend(request),
563
+ recoverSend: (request) => adapter.recoverSend(request),
564
+ executeFinalTabTransaction: (request) => adapter.executeFinalTabTransaction(request),
565
+ establishTarget: (request) => this.establishTarget(request),
566
+ persistActionIntent: (request) => this.persistActionIntent(request),
567
+ persistReceiptEvidence: (request) => this.persistSubmissionEvidence(request),
568
+ persistOwnershipBaseline: (request) => this.persistOwnershipBaseline(request)
569
+ };
570
+ }
571
+ async stageRequest(request, requestDigest, targetBindingDigest, adapter, signal, deadlineAt) {
572
+ for (const kind of stagingKinds(request)) {
573
+ const stage = {
574
+ operationId: request.operationId,
575
+ requestDigest,
576
+ targetBindingDigest,
577
+ actionId: await this.stagingActionId(request.operationId, requestDigest, kind),
578
+ kind,
579
+ desiredStateDigest: this.journal.evidenceDigest("staging-desired", { requestDigest, kind })
580
+ };
581
+ const result = await runOperationStaging(stage, {
582
+ readCurrent: callback => adapter.readCurrent(callback),
583
+ persistIntent: intent => this.persistStagingIntent(intent.identity),
584
+ mutateOnce: callback => adapter.mutateOnce(callback),
585
+ observe: callback => adapter.observe(callback),
586
+ persistReceipt: receipt => this.persistStagingReceipt(receipt.receipt)
587
+ }, {
588
+ signal,
589
+ ...(deadlineAt === undefined ? {} : { deadlineAt }),
590
+ now: this.now
591
+ });
592
+ if (result.kind !== "completed") {
593
+ return this.submissionFromStagingBlocker(request, requestDigest, targetBindingDigest, result);
594
+ }
595
+ }
596
+ return undefined;
597
+ }
598
+ async stagingActionId(operationId, requestDigest, kind) {
599
+ // An unsettled intent is the only action ID that can be resumed. Settled
600
+ // set-to-value actions receive a fresh ID so a later exact observation may
601
+ // authorize one new reconciliation without reusing an earlier mutation.
602
+ // The caller still cannot create two concurrent mutations: target actor
603
+ // serialization plus the staging core's fresh post-intent observation
604
+ // makes the second action observation-only when the first already won.
605
+ const current = await this.journal.load(operationId, requestDigest);
606
+ const unsettled = Object.values(current.state.actions).filter(action => action.kind === kind && action.outcome === undefined);
607
+ if (unsettled.length > 1) {
608
+ throw new OperationServiceError("operation_state_corrupt", "Operation contains duplicate unsettled staging actions.");
609
+ }
610
+ return unsettled[0]?.actionId ?? randomUUID();
611
+ }
612
+ async persistStagingIntent(identity) {
613
+ const current = await this.journal.load(identity.operationId, identity.requestDigest);
614
+ const existing = current.state.actions[identity.actionId];
615
+ if (existing !== undefined) {
616
+ if (existing.kind !== identity.kind
617
+ || existing.repeatPolicy !== "reconcile_set_to_value"
618
+ || existing.requestDigest !== identity.requestDigest
619
+ || existing.targetDigest !== identity.targetBindingDigest) {
620
+ throw new OperationServiceError("operation_state_corrupt", "Staging action identity conflicts with durable state.");
621
+ }
622
+ if (existing.outcome === undefined)
623
+ return { status: "existing_unsettled" };
624
+ return {
625
+ status: "existing_settled",
626
+ receipt: stagingReceiptFromAction(identity, existing)
627
+ };
628
+ }
629
+ await this.persistActionIntent({
630
+ operationId: identity.operationId,
631
+ requestDigest: identity.requestDigest,
632
+ surface: current.state.surface,
633
+ actionId: identity.actionId,
634
+ kind: identity.kind,
635
+ repeatPolicy: "reconcile_set_to_value",
636
+ targetBindingDigest: identity.targetBindingDigest
637
+ });
638
+ return { status: "created" };
639
+ }
640
+ async persistStagingReceipt(receipt) {
641
+ await this.appendActionReceiptConvergent(receipt.operationId, receipt.requestDigest, receipt.actionId, receipt.outcome, receipt.currentStateDigest ?? receipt.evidenceDigest, receipt.blockerCode);
642
+ }
643
+ submissionFromStagingBlocker(request, requestDigest, targetBindingDigest, result) {
644
+ const code = stagingBlockerCode(result.stagingKind, result.blocker.code);
645
+ return {
646
+ operationId: request.operationId,
647
+ requestDigest,
648
+ surface: request.surface,
649
+ targetBindingDigest,
650
+ kind: result.kind === "uncertain" ? "uncertain" : "blocked",
651
+ blocker: {
652
+ code,
653
+ observationRequired: result.blocker.observationRequired,
654
+ mutationBoundary: "none",
655
+ ...(result.blocker.evidenceDigest === undefined ? {} : { evidenceDigest: result.blocker.evidenceDigest })
656
+ }
657
+ };
658
+ }
659
+ async persistActionIntent(request) {
660
+ if (!UUID_PATTERN.test(request.actionId) || !DIGEST_PATTERN.test(request.requestDigest) || !DIGEST_PATTERN.test(request.targetBindingDigest)) {
661
+ throw new OperationServiceError("invalid_action_intent", "Action intent identity is invalid.");
662
+ }
663
+ const durableRequestDigest = request.durableRequestDigest ?? request.requestDigest;
664
+ let afterIntent;
665
+ for (let attempt = 0; attempt < this.maxCasRetries; attempt += 1) {
666
+ const current = await this.journal.load(request.operationId, durableRequestDigest);
667
+ const existing = current.state.actions[request.actionId];
668
+ if (existing !== undefined) {
669
+ if (!actionMatchesIntent(existing, request)) {
670
+ throw new OperationServiceError("operation_state_corrupt", "Action ID is already bound to a different intent.");
671
+ }
672
+ afterIntent = current;
673
+ break;
674
+ }
675
+ // This check must run again after every CAS conflict. Checking once and
676
+ // then using a generic convergent append allows two callers with
677
+ // different action IDs to serialize two intents for the same one-shot
678
+ // action. Reversible staging similarly permits at most one *unsettled*
679
+ // intent per kind; a later settled action may be reconciled again.
680
+ const conflict = conflictingActionForIntent(current.state, request);
681
+ if (conflict !== undefined) {
682
+ throw new OperationServiceError("action_already_intended", `A ${request.kind} intent already exists for this operation.`);
683
+ }
684
+ const event = {
685
+ type: "action_intent",
686
+ action: {
687
+ actionId: request.actionId,
688
+ kind: request.kind,
689
+ repeatPolicy: request.repeatPolicy,
690
+ requestDigest: request.requestDigest,
691
+ targetDigest: request.targetBindingDigest
692
+ },
693
+ intentAt: this.timestamp(current.state.updatedAt)
694
+ };
695
+ try {
696
+ afterIntent = await this.journal.append(request.operationId, current.state.revision, event);
697
+ break;
698
+ }
699
+ catch (error) {
700
+ try {
701
+ const observed = await this.journal.load(request.operationId, durableRequestDigest);
702
+ const committed = observed.state.actions[request.actionId];
703
+ if (committed !== undefined) {
704
+ if (!actionMatchesIntent(committed, request)) {
705
+ throw new OperationServiceError("operation_state_corrupt", "Committed action intent conflicts with the caller identity.");
706
+ }
707
+ afterIntent = observed;
708
+ break;
709
+ }
710
+ if (conflictingActionForIntent(observed.state, request) !== undefined) {
711
+ throw new OperationServiceError("action_already_intended", `A ${request.kind} intent already exists for this operation.`);
712
+ }
713
+ }
714
+ catch (reconcileError) {
715
+ if (reconcileError instanceof OperationServiceError)
716
+ throw reconcileError;
717
+ // Preserve the original append error when the committed prefix
718
+ // cannot be authenticated. Never reinterpret corruption as a retry.
719
+ }
720
+ if (isRevisionConflict(error))
721
+ continue;
722
+ throw this.serviceError(error, "journal_unavailable");
723
+ }
724
+ }
725
+ if (afterIntent === undefined) {
726
+ throw new OperationServiceError("journal_conflict", "Concurrent operation writers did not converge within the retry bound.");
727
+ }
728
+ if (request.kind === "file_handoff" && afterIntent.state.phase === "prepared") {
729
+ await this.appendPhaseConvergent(request.operationId, durableRequestDigest, "handoff_pending", request.actionId);
730
+ }
731
+ else if (request.kind === "send" && afterIntent.state.phase === "ready") {
732
+ await this.appendPhaseConvergent(request.operationId, durableRequestDigest, "send_pending", request.actionId);
733
+ }
734
+ }
735
+ /**
736
+ * Persist the non-repeatable Send intent and its complete ownership anchor
737
+ * in one event. `executeAllowed` is true only when this exact invocation's
738
+ * append and adjacent phase transition both returned successfully. Any
739
+ * commit-then-throw or concurrent convergence is observation-only.
740
+ */
741
+ async persistPreparedSend(request) {
742
+ if (!UUID_PATTERN.test(request.operationId)
743
+ || !UUID_PATTERN.test(request.actionId)
744
+ || !DIGEST_PATTERN.test(request.requestDigest)
745
+ || !DIGEST_PATTERN.test(request.durableRequestDigest)
746
+ || !DIGEST_PATTERN.test(request.targetBindingDigest)
747
+ || request.requestDigest !== request.durableRequestDigest
748
+ || request.kind !== "send"
749
+ || request.repeatPolicy !== "observe_only_after_intent"
750
+ || (request.surface !== "chat" && request.surface !== "work")) {
751
+ return { status: "not_committed", blockerCode: "operation_state_corrupt" };
752
+ }
753
+ let baselineSnapshot;
754
+ try {
755
+ baselineSnapshot = JSON.parse(canonicalJson(request.baseline));
756
+ }
757
+ catch {
758
+ return { status: "not_committed", blockerCode: "operation_state_corrupt" };
759
+ }
760
+ const intentRequest = {
761
+ operationId: request.operationId,
762
+ requestDigest: request.requestDigest,
763
+ durableRequestDigest: request.durableRequestDigest,
764
+ surface: request.surface,
765
+ actionId: request.actionId,
766
+ kind: "send",
767
+ repeatPolicy: "observe_only_after_intent",
768
+ targetBindingDigest: request.targetBindingDigest
769
+ };
770
+ for (let attempt = 0; attempt < this.maxCasRetries; attempt += 1) {
771
+ let current;
772
+ try {
773
+ current = await this.journal.load(request.operationId, request.durableRequestDigest);
774
+ }
775
+ catch {
776
+ return { status: "uncertain" };
777
+ }
778
+ if (current.state.surface !== request.surface
779
+ || current.state.target === undefined
780
+ || this.targetBindingDigest(current.state) !== request.targetBindingDigest) {
781
+ return { status: "not_committed", blockerCode: "target_binding_mismatch" };
782
+ }
783
+ const baseline = {
784
+ schemaVersion: OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION,
785
+ operationId: request.operationId,
786
+ requestDigest: request.requestDigest,
787
+ targetBindingDigest: request.targetBindingDigest,
788
+ actionId: request.actionId,
789
+ baseline: baselineSnapshot,
790
+ observedAt: current.state.updatedAt
791
+ };
792
+ try {
793
+ assertOwnershipBaselineShape(baseline);
794
+ }
795
+ catch {
796
+ return { status: "not_committed", blockerCode: "operation_state_corrupt" };
797
+ }
798
+ const existing = current.state.actions[request.actionId];
799
+ if (existing !== undefined) {
800
+ const durableBaseline = current.state.ownershipBaselines?.[request.actionId]
801
+ ?? (existing.kind === "send" ? current.state.ownershipBaseline : undefined);
802
+ if (!actionMatchesIntent(existing, intentRequest)
803
+ || durableBaseline === undefined
804
+ || canonicalJson(durableBaseline) !== canonicalJson({
805
+ ...baseline,
806
+ observedAt: durableBaseline.observedAt
807
+ })) {
808
+ return { status: "not_committed", blockerCode: "operation_state_corrupt" };
809
+ }
810
+ try {
811
+ if (current.state.phase === "ready") {
812
+ await this.appendPhaseConvergent(request.operationId, request.durableRequestDigest, "send_pending", request.actionId);
813
+ }
814
+ }
815
+ catch {
816
+ return { status: "uncertain" };
817
+ }
818
+ return { status: "committed", executeAllowed: false };
819
+ }
820
+ if (conflictingActionForIntent(current.state, intentRequest) !== undefined) {
821
+ // A concurrent writer used another random action ID. The caller must
822
+ // reload the operation so recovery uses that exact durable identity.
823
+ return { status: "not_committed", blockerCode: "stale_handle" };
824
+ }
825
+ const event = {
826
+ type: "action_prepared",
827
+ action: {
828
+ actionId: request.actionId,
829
+ kind: "send",
830
+ repeatPolicy: "observe_only_after_intent",
831
+ requestDigest: request.requestDigest,
832
+ targetDigest: request.targetBindingDigest
833
+ },
834
+ intentAt: current.state.updatedAt,
835
+ baseline
836
+ };
837
+ try {
838
+ await this.journal.append(request.operationId, current.state.revision, event);
839
+ await this.appendPhaseConvergent(request.operationId, request.durableRequestDigest, "send_pending", request.actionId);
840
+ return { status: "committed", executeAllowed: true };
841
+ }
842
+ catch (error) {
843
+ // A committed append whose acknowledgement was lost is safe to
844
+ // recover, but never safe to execute from this invocation.
845
+ try {
846
+ const observed = await this.journal.load(request.operationId, request.durableRequestDigest);
847
+ const action = observed.state.actions[request.actionId];
848
+ const durableBaseline = observed.state.ownershipBaselines?.[request.actionId]
849
+ ?? (action?.kind === "send" ? observed.state.ownershipBaseline : undefined);
850
+ if (action !== undefined
851
+ && actionMatchesIntent(action, intentRequest)
852
+ && durableBaseline !== undefined
853
+ && canonicalJson(durableBaseline) === canonicalJson({ ...baseline, observedAt: durableBaseline.observedAt })) {
854
+ try {
855
+ if (observed.state.phase === "ready") {
856
+ await this.appendPhaseConvergent(request.operationId, request.durableRequestDigest, "send_pending", request.actionId);
857
+ }
858
+ }
859
+ catch {
860
+ return { status: "uncertain" };
861
+ }
862
+ return { status: "committed", executeAllowed: false };
863
+ }
864
+ if (conflictingActionForIntent(observed.state, intentRequest) !== undefined) {
865
+ return { status: "not_committed", blockerCode: "stale_handle" };
866
+ }
867
+ }
868
+ catch {
869
+ return { status: "uncertain" };
870
+ }
871
+ if (isRevisionConflict(error))
872
+ continue;
873
+ return { status: "uncertain" };
874
+ }
875
+ }
876
+ return { status: "uncertain" };
877
+ }
878
+ /**
879
+ * Atomically fence a Work-steer action with its complete pre-steer
880
+ * ownership anchor. The action_prepared event is the sole mutation
881
+ * authorization boundary: an invocation that observes or converges a
882
+ * committed prefix is permanently recovery-only. A different unresolved
883
+ * action receives an explicit typed block; a previously settled action does
884
+ * not prevent a new caller-owned steer.
885
+ */
886
+ async persistSteerIntentAndBaseline(request) {
887
+ const schemaVersion = CONTROL_COORDINATOR_SCHEMA_VERSION;
888
+ if (request.schemaVersion !== schemaVersion
889
+ || request.action !== "steer"
890
+ || !UUID_PATTERN.test(request.parentOperationId)
891
+ || !UUID_PATTERN.test(request.controlActionId)
892
+ || !DIGEST_PATTERN.test(request.parentRequestDigest)
893
+ || !DIGEST_PATTERN.test(request.parentTargetBindingDigest)
894
+ || !DIGEST_PATTERN.test(request.requestDigest)
895
+ || request.requestDigest === request.parentRequestDigest
896
+ || !DIGEST_PATTERN.test(request.baselineSnapshotDigest)
897
+ || !DIGEST_PATTERN.test(request.preparedDigest)
898
+ || !isSafeOpaqueId(request.expectedAssistantTurnId)
899
+ || !isSafeOpaqueId(request.assistantBranchId)
900
+ || !isSafeOpaqueId(request.assistantParentTurnId)) {
901
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer persistence identity is invalid.");
902
+ }
903
+ let material;
904
+ try {
905
+ material = controlSteerPreparedDigestMaterial({
906
+ parentOperationId: request.parentOperationId,
907
+ parentRequestDigest: request.parentRequestDigest,
908
+ parentTargetBindingDigest: request.parentTargetBindingDigest,
909
+ controlActionId: request.controlActionId,
910
+ expectedAssistantTurnId: request.expectedAssistantTurnId,
911
+ assistantBranchId: request.assistantBranchId,
912
+ assistantParentTurnId: request.assistantParentTurnId,
913
+ baselineSnapshotDigest: request.baselineSnapshotDigest,
914
+ baseline: request.baseline
915
+ });
916
+ }
917
+ catch {
918
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer persistence baseline is invalid.");
919
+ }
920
+ const expectedPreparedDigest = this.journal.evidenceDigest("work-steer-prepared", material);
921
+ if (expectedPreparedDigest !== request.preparedDigest) {
922
+ throw new OperationServiceError("operation_request_mismatch", "Work-steer prepared evidence does not match the journal identity.");
923
+ }
924
+ const intentRequest = {
925
+ operationId: request.parentOperationId,
926
+ requestDigest: request.requestDigest,
927
+ durableRequestDigest: request.parentRequestDigest,
928
+ surface: "work",
929
+ actionId: request.controlActionId,
930
+ kind: "work_steer",
931
+ repeatPolicy: "observe_only_after_intent",
932
+ targetBindingDigest: request.parentTargetBindingDigest
933
+ };
934
+ for (let attempt = 0; attempt < this.maxCasRetries; attempt += 1) {
935
+ let current;
936
+ try {
937
+ current = await this.journal.load(request.parentOperationId, request.parentRequestDigest);
938
+ }
939
+ catch (error) {
940
+ throw this.serviceError(error, "journal_unavailable");
941
+ }
942
+ if (current.state.target === undefined
943
+ || current.state.surface !== "work"
944
+ || this.targetBindingDigest(current.state) !== request.parentTargetBindingDigest) {
945
+ throw new OperationServiceError("target_binding_mismatch", "Work-steer target or surface does not match durable state.");
946
+ }
947
+ const existing = current.state.actions[request.controlActionId];
948
+ if (existing !== undefined) {
949
+ if (!actionMatchesIntent(existing, intentRequest)) {
950
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer action ID is already bound to a different intent.");
951
+ }
952
+ const durableBaseline = current.state.ownershipBaselines?.[request.controlActionId];
953
+ if (durableBaseline === undefined || canonicalJson(durableBaseline) !== canonicalJson({
954
+ schemaVersion: OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION,
955
+ operationId: request.parentOperationId,
956
+ requestDigest: request.parentRequestDigest,
957
+ targetBindingDigest: request.parentTargetBindingDigest,
958
+ actionId: request.controlActionId,
959
+ baseline: material.baseline,
960
+ observedAt: durableBaseline?.observedAt
961
+ })) {
962
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer baseline does not match the prepared request.");
963
+ }
964
+ const durablePrepared = this.reconstructSteerIntent(current.state, request.controlActionId, request.parentRequestDigest, request.parentTargetBindingDigest, request.requestDigest, request.expectedAssistantTurnId);
965
+ if (durablePrepared.preparedDigest !== request.preparedDigest) {
966
+ throw new OperationServiceError("operation_request_mismatch", "Durable Work-steer prepared evidence conflicts with the request.");
967
+ }
968
+ return { schemaVersion, disposition: "same_action_recovery" };
969
+ }
970
+ const conflicting = Object.values(current.state.actions).find(action => {
971
+ if (action.kind !== "work_steer" || action.actionId === request.controlActionId)
972
+ return false;
973
+ return priorWorkSteerDisposition(current.state, action) === "unresolved";
974
+ });
975
+ if (conflicting !== undefined) {
976
+ // A different action ID is still pending or uncertain. The caller
977
+ // must not append a second fence or attempt a mutation concurrently.
978
+ return {
979
+ schemaVersion,
980
+ disposition: "blocked",
981
+ blockerCode: "provider_concurrency_unsupported"
982
+ };
983
+ }
984
+ const intentAt = current.state.updatedAt;
985
+ const baseline = {
986
+ schemaVersion: OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION,
987
+ operationId: request.parentOperationId,
988
+ requestDigest: request.parentRequestDigest,
989
+ targetBindingDigest: request.parentTargetBindingDigest,
990
+ actionId: request.controlActionId,
991
+ baseline: material.baseline,
992
+ observedAt: intentAt
993
+ };
994
+ try {
995
+ assertOwnershipBaselineShape(baseline);
996
+ }
997
+ catch {
998
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer ownership baseline is invalid.");
999
+ }
1000
+ const event = {
1001
+ type: "action_prepared",
1002
+ action: {
1003
+ actionId: request.controlActionId,
1004
+ kind: "work_steer",
1005
+ repeatPolicy: "observe_only_after_intent",
1006
+ requestDigest: request.requestDigest,
1007
+ targetDigest: request.parentTargetBindingDigest
1008
+ },
1009
+ intentAt,
1010
+ baseline
1011
+ };
1012
+ try {
1013
+ const committed = await this.journal.append(request.parentOperationId, current.state.revision, event);
1014
+ const durableBaseline = committed.state.ownershipBaselines?.[request.controlActionId];
1015
+ const durableAction = committed.state.actions[request.controlActionId];
1016
+ if (durableAction === undefined
1017
+ || !actionMatchesIntent(durableAction, intentRequest)
1018
+ || durableBaseline === undefined
1019
+ || canonicalJson(durableBaseline) !== canonicalJson(baseline)) {
1020
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer action_prepared append did not converge to the requested identity.");
1021
+ }
1022
+ // This is the only path that may authorize the request-local execute
1023
+ // port. A successful append acknowledgement proves this invocation
1024
+ // acquired the journal fence; no subsequent convergence path does.
1025
+ return { schemaVersion, disposition: "acquired" };
1026
+ }
1027
+ catch (error) {
1028
+ try {
1029
+ const observed = await this.journal.load(request.parentOperationId, request.parentRequestDigest);
1030
+ const committedAction = observed.state.actions[request.controlActionId];
1031
+ if (committedAction !== undefined) {
1032
+ if (!actionMatchesIntent(committedAction, intentRequest)) {
1033
+ throw new OperationServiceError("operation_state_corrupt", "Committed Work-steer action conflicts with the caller identity.");
1034
+ }
1035
+ const committedBaseline = observed.state.ownershipBaselines?.[request.controlActionId];
1036
+ if (committedBaseline === undefined || canonicalJson(committedBaseline) !== canonicalJson({
1037
+ ...baseline,
1038
+ observedAt: committedBaseline.observedAt
1039
+ })) {
1040
+ throw new OperationServiceError("operation_state_corrupt", "Committed Work-steer baseline is not authenticated.");
1041
+ }
1042
+ // Append may have committed before its acknowledgement failed.
1043
+ // That invocation is recovery-only and must never execute.
1044
+ return { schemaVersion, disposition: "same_action_recovery" };
1045
+ }
1046
+ const conflicting = Object.values(observed.state.actions).find(action => {
1047
+ if (action.kind !== "work_steer" || action.actionId === request.controlActionId)
1048
+ return false;
1049
+ return priorWorkSteerDisposition(observed.state, action) === "unresolved";
1050
+ });
1051
+ if (conflicting !== undefined) {
1052
+ return {
1053
+ schemaVersion,
1054
+ disposition: "blocked",
1055
+ blockerCode: "provider_concurrency_unsupported"
1056
+ };
1057
+ }
1058
+ }
1059
+ catch (reconcileError) {
1060
+ if (reconcileError instanceof OperationServiceError)
1061
+ throw reconcileError;
1062
+ }
1063
+ if (isRevisionConflict(error))
1064
+ continue;
1065
+ throw this.serviceError(error, "journal_unavailable");
1066
+ }
1067
+ }
1068
+ throw new OperationServiceError("journal_conflict", "Concurrent Work-steer writers did not converge within the retry bound.");
1069
+ }
1070
+ async persistSubmissionEvidence(request) {
1071
+ if (request.kind === "phase") {
1072
+ if (request.actionOutcome !== undefined && request.actionId !== undefined) {
1073
+ await this.appendActionReceiptConvergent(request.operationId, request.requestDigest, request.actionId, request.actionOutcome, request.evidenceDigest);
1074
+ }
1075
+ if (request.phase === "ready") {
1076
+ await this.appendPhaseConvergent(request.operationId, request.requestDigest, "ready", request.actionId, request.evidenceDigest);
1077
+ }
1078
+ return;
1079
+ }
1080
+ if (request.kind === "receipt") {
1081
+ let loaded = await this.journal.load(request.operationId, request.requestDigest);
1082
+ const send = originalSendAction(loaded.state);
1083
+ if (send === undefined)
1084
+ throw new OperationServiceError("operation_state_inconsistent", "Receipt evidence has no durable submission intent.");
1085
+ if (loaded.state.submissionWitness !== undefined) {
1086
+ if (loaded.state.ownershipBaseline === undefined
1087
+ || !submissionWitnessMatchesReceipt(loaded.state.submissionWitness, request, send.actionId, loaded.state.ownershipBaseline.baseline.snapshotDigest)) {
1088
+ throw new OperationServiceError("submission_witness_conflict", "Receipt evidence conflicts with the durable submission witness.");
1089
+ }
1090
+ }
1091
+ else {
1092
+ if (loaded.state.ownershipBaseline === undefined) {
1093
+ throw new OperationServiceError("ownership_baseline_missing", "Receipt evidence requires the durable pre-Send ownership baseline.");
1094
+ }
1095
+ const witness = this.submissionWitnessFromReceipt(request, send.actionId, loaded.state.ownershipBaseline.baseline.snapshotDigest, loaded.state.updatedAt);
1096
+ loaded = await this.appendSubmissionWitnessConvergent(request.operationId, request.requestDigest, witness);
1097
+ }
1098
+ if (loaded.state.phase === "ready") {
1099
+ if (send.outcome === undefined) {
1100
+ loaded = await this.appendPhaseConvergent(request.operationId, request.requestDigest, "send_pending", send.actionId);
1101
+ }
1102
+ else {
1103
+ loaded = await this.appendPhaseConvergent(request.operationId, request.requestDigest, "uncertain", send.actionId, request.evidenceDigest);
1104
+ }
1105
+ }
1106
+ if (send.outcome === undefined) {
1107
+ await this.appendActionReceiptConvergent(request.operationId, request.requestDigest, send.actionId, "satisfied", request.evidenceDigest);
1108
+ }
1109
+ await this.appendPhaseConvergent(request.operationId, request.requestDigest, "submitted", send.actionId, request.evidenceDigest);
1110
+ return;
1111
+ }
1112
+ const state = await this.journal.load(request.operationId, request.requestDigest);
1113
+ const messageDigest = this.journal.evidenceDigest("blocker", {
1114
+ code: request.blocker.code,
1115
+ evidenceDigest: request.blocker.evidenceDigest
1116
+ });
1117
+ const blockerEvent = {
1118
+ type: "blocker_observed",
1119
+ blocker: {
1120
+ code: request.blocker.code,
1121
+ messageDigest,
1122
+ recoverable: request.blocker.observationRequired,
1123
+ observedAt: this.timestamp(state.state.updatedAt)
1124
+ }
1125
+ };
1126
+ await this.appendConvergent(request.operationId, request.requestDigest, blockerEvent, current => current.lastBlocker?.code === request.blocker.code && current.lastBlocker.messageDigest === messageDigest);
1127
+ const after = await this.journal.load(request.operationId, request.requestDigest);
1128
+ if (after.state.phase !== "uncertain"
1129
+ && after.state.phase !== "completed"
1130
+ && request.blocker.observationRequired
1131
+ && request.blocker.mutationBoundary !== "none"
1132
+ && after.state.mutationBoundary !== "none"
1133
+ && UNCERTAIN_SUBMISSION_BLOCKERS.has(request.blocker.code)) {
1134
+ await this.appendPhaseConvergent(request.operationId, request.requestDigest, "uncertain", undefined, request.blocker.evidenceDigest);
1135
+ }
1136
+ }
1137
+ async persistReturnedSubmissionBlocker(result) {
1138
+ if (result.kind !== "blocked" && result.kind !== "cancelled" && result.kind !== "uncertain")
1139
+ return;
1140
+ try {
1141
+ const current = await this.journal.load(result.operationId, result.requestDigest);
1142
+ await this.persistSubmissionEvidence({
1143
+ kind: "blocker",
1144
+ operationId: result.operationId,
1145
+ requestDigest: result.requestDigest,
1146
+ surface: result.surface,
1147
+ phase: current.state.phase,
1148
+ mutationBoundary: current.state.mutationBoundary,
1149
+ ...(result.targetBindingDigest === undefined ? {} : { targetBindingDigest: result.targetBindingDigest }),
1150
+ blocker: result.blocker
1151
+ });
1152
+ }
1153
+ catch {
1154
+ // The browser result still carries the blocker and every non-repeatable
1155
+ // intent was persisted before mutation. A diagnostic append failure must
1156
+ // not manufacture a different browser outcome or authorize a retry.
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Append the one immutable post-Send ownership witness. This is deliberately
1161
+ * separate from action receipts and phase events: callers must be able to
1162
+ * replay a crash prefix and distinguish "Send was invoked" from "the exact
1163
+ * operation-owned user turn was proven" without consulting the browser.
1164
+ */
1165
+ async appendSubmissionWitnessConvergent(operationId, requestDigest, witness) {
1166
+ validateSubmissionWitnessInput(witness);
1167
+ let current = await this.journal.load(operationId, requestDigest);
1168
+ const action = current.state.actions[witness.actionId];
1169
+ if (action === undefined || action.kind !== witness.actionKind) {
1170
+ throw new OperationServiceError("submission_witness_action_missing", "Submission witness does not name its durable causal action.");
1171
+ }
1172
+ if (action.targetDigest !== witness.targetBindingDigest) {
1173
+ throw new OperationServiceError("submission_witness_target_mismatch", "Submission witness target does not match its causal action.");
1174
+ }
1175
+ if (action.outcome === "not_satisfied" || action.outcome === "uncertain") {
1176
+ throw new OperationServiceError("submission_witness_action_unproven", "Submission witness cannot follow an unsatisfied or uncertain causal action.");
1177
+ }
1178
+ const durableBaseline = current.state.ownershipBaselines?.[witness.actionId]
1179
+ ?? (action.kind === "send" && current.state.ownershipBaseline?.actionId === witness.actionId
1180
+ ? current.state.ownershipBaseline
1181
+ : undefined);
1182
+ if (durableBaseline === undefined) {
1183
+ throw new OperationServiceError("ownership_baseline_missing", "A submission witness requires the durable baseline for its causal action.");
1184
+ }
1185
+ if (durableBaseline.operationId !== operationId
1186
+ || durableBaseline.requestDigest !== requestDigest
1187
+ || durableBaseline.targetBindingDigest !== witness.targetBindingDigest
1188
+ || durableBaseline.actionId !== witness.actionId
1189
+ || witness.baselineSnapshotDigest !== durableBaseline.baseline.snapshotDigest) {
1190
+ throw new OperationServiceError("submission_witness_baseline_mismatch", "Submission witness is not bound to its durable causal baseline.");
1191
+ }
1192
+ const existing = submissionWitnessForAction(current.state, action);
1193
+ if (existing !== undefined) {
1194
+ if (canonicalJson(existing) !== canonicalJson(witness)) {
1195
+ throw new OperationServiceError("submission_witness_conflict", "A different immutable submission witness is already durable for this action.");
1196
+ }
1197
+ return current;
1198
+ }
1199
+ const event = {
1200
+ type: "submission_witness",
1201
+ witness
1202
+ };
1203
+ current = await this.appendConvergent(operationId, requestDigest, event, state => {
1204
+ const persistedAction = state.actions[witness.actionId];
1205
+ const persisted = persistedAction === undefined
1206
+ ? undefined
1207
+ : submissionWitnessForAction(state, persistedAction);
1208
+ return persisted !== undefined && canonicalJson(persisted) === canonicalJson(witness);
1209
+ });
1210
+ const persisted = submissionWitnessForAction(current.state, action);
1211
+ if (persisted === undefined || canonicalJson(persisted) !== canonicalJson(witness)) {
1212
+ throw new OperationServiceError("submission_witness_indeterminate", "Submission witness was not durably validated after persistence.");
1213
+ }
1214
+ return current;
1215
+ }
1216
+ submissionWitnessFromEstablishment(establishment, baselineSnapshotDigest, notBefore) {
1217
+ if (establishment.postSendDeltaDigest === undefined) {
1218
+ throw new OperationServiceError("target_establishment_delta_missing", "New-target establishment requires exact post-Send delta evidence.");
1219
+ }
1220
+ if (baselineSnapshotDigest === undefined) {
1221
+ throw new OperationServiceError("ownership_baseline_missing", "New-target establishment requires the durable pre-Send ownership baseline.");
1222
+ }
1223
+ const observedAt = notBefore !== undefined && Date.parse(establishment.observedAt) < Date.parse(notBefore)
1224
+ ? notBefore
1225
+ : establishment.observedAt;
1226
+ return {
1227
+ schemaVersion: OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION,
1228
+ actionId: establishment.causalSendActionId,
1229
+ actionKind: "send",
1230
+ targetBindingDigest: establishment.targetBindingDigest,
1231
+ baselineSnapshotDigest,
1232
+ postSendDeltaDigest: establishment.postSendDeltaDigest,
1233
+ operationUserEvidenceDigest: establishment.userTurnEvidenceDigest,
1234
+ userTurnId: establishment.userTurnId,
1235
+ observedAt
1236
+ };
1237
+ }
1238
+ submissionWitnessFromReceipt(receipt, actionId, baselineSnapshotDigest, notBefore) {
1239
+ return {
1240
+ schemaVersion: OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION,
1241
+ actionId,
1242
+ actionKind: "send",
1243
+ targetBindingDigest: receipt.targetBindingDigest,
1244
+ baselineSnapshotDigest,
1245
+ postSendDeltaDigest: receipt.postSendDeltaDigest,
1246
+ operationUserEvidenceDigest: receipt.userTurnEvidenceDigest,
1247
+ userTurnId: receipt.userTurnId,
1248
+ observedAt: receipt.observedAt ?? this.timestamp(notBefore)
1249
+ };
1250
+ }
1251
+ async appendActionReceiptConvergent(operationId, requestDigest, actionId, outcome, evidenceDigest, blockerCode, observedAt) {
1252
+ const current = await this.journal.load(operationId, requestDigest);
1253
+ const action = current.state.actions[actionId];
1254
+ if (action === undefined)
1255
+ throw new OperationServiceError("operation_state_inconsistent", "Action receipt has no durable intent.");
1256
+ if (action.outcome !== undefined) {
1257
+ if (action.outcome !== outcome)
1258
+ throw new OperationServiceError("operation_state_corrupt", "Action receipt outcome conflicts with durable state.");
1259
+ if (evidenceDigest !== undefined && action.evidenceDigest !== evidenceDigest) {
1260
+ throw new OperationServiceError("operation_state_corrupt", "Action receipt evidence conflicts with durable state.");
1261
+ }
1262
+ if (blockerCode !== undefined && action.blockerCode !== blockerCode) {
1263
+ throw new OperationServiceError("operation_state_corrupt", "Action receipt blocker conflicts with durable state.");
1264
+ }
1265
+ return current;
1266
+ }
1267
+ if (outcome === "satisfied" && (evidenceDigest === undefined || !DIGEST_PATTERN.test(evidenceDigest))) {
1268
+ throw new OperationServiceError("invalid_action_receipt", "A satisfied action requires canonical evidence.");
1269
+ }
1270
+ const event = {
1271
+ type: "action_receipt",
1272
+ actionId,
1273
+ outcome,
1274
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest }),
1275
+ ...(blockerCode === undefined ? {} : { blockerCode }),
1276
+ observedAt: observedAt ?? this.timestamp(action.intentAt)
1277
+ };
1278
+ return this.appendConvergent(operationId, requestDigest, event, state => state.actions[actionId]?.outcome === outcome);
1279
+ }
1280
+ /**
1281
+ * Append the complete redacted pre-Send ownership baseline. The callback is
1282
+ * invoked by the adapter after its final precondition read and before its
1283
+ * sole browser activation. Consequently a process crash can leave either a
1284
+ * durable baseline with no click, or a durable baseline plus a Send intent;
1285
+ * it can never require reconstructing authority from a post-Send page.
1286
+ */
1287
+ async persistOwnershipBaseline(request) {
1288
+ const current = await this.journal.load(request.operationId, request.requestDigest);
1289
+ const action = current.state.actions[request.actionId];
1290
+ if (action === undefined
1291
+ || (action.kind !== "send" && action.kind !== "work_steer")
1292
+ || action.targetDigest !== request.targetBindingDigest
1293
+ || action.requestDigest !== request.requestDigest
1294
+ || action.outcome !== undefined) {
1295
+ throw new OperationServiceError("ownership_baseline_action_invalid", "The pre-Send baseline is not adjacent to an unsettled causal action intent.");
1296
+ }
1297
+ const baseline = {
1298
+ schemaVersion: OPERATION_OWNERSHIP_BASELINE_SCHEMA_VERSION,
1299
+ operationId: request.operationId,
1300
+ requestDigest: request.requestDigest,
1301
+ targetBindingDigest: request.targetBindingDigest,
1302
+ actionId: request.actionId,
1303
+ baseline: request.baseline,
1304
+ // Derive the wrapper timestamp from the immutable loaded prefix so
1305
+ // concurrent identical writers produce byte-identical events and
1306
+ // converge instead of conflicting on wall-clock jitter.
1307
+ observedAt: current.state.updatedAt
1308
+ };
1309
+ try {
1310
+ assertOwnershipBaselineShape(baseline);
1311
+ }
1312
+ catch {
1313
+ throw new OperationServiceError("ownership_baseline_invalid", "The provider returned an invalid or unbounded pre-Send ownership baseline.");
1314
+ }
1315
+ const existing = current.state.ownershipBaseline;
1316
+ if (existing !== undefined) {
1317
+ if (canonicalJson(existing) !== canonicalJson(baseline)) {
1318
+ throw new OperationServiceError("ownership_baseline_conflict", "A different immutable pre-Send ownership baseline is already durable.");
1319
+ }
1320
+ return;
1321
+ }
1322
+ const event = {
1323
+ type: "ownership_baseline",
1324
+ baseline
1325
+ };
1326
+ const loaded = await this.appendConvergent(request.operationId, request.requestDigest, event, state => state.ownershipBaseline !== undefined && canonicalJson(state.ownershipBaseline) === canonicalJson(baseline));
1327
+ if (loaded.state.ownershipBaseline === undefined || canonicalJson(loaded.state.ownershipBaseline) !== canonicalJson(baseline)) {
1328
+ throw new OperationServiceError("ownership_baseline_indeterminate", "The pre-Send ownership baseline was not durably validated after persistence.");
1329
+ }
1330
+ }
1331
+ async appendPhaseConvergent(operationId, requestDigest, phase, causeActionId, evidenceDigest) {
1332
+ for (let attempt = 0; attempt < this.maxCasRetries; attempt += 1) {
1333
+ const loaded = await this.journal.load(operationId, requestDigest);
1334
+ if (phaseReached(loaded.state.phase, phase))
1335
+ return loaded;
1336
+ if (loaded.state.phase === "completed")
1337
+ return loaded;
1338
+ const event = {
1339
+ type: "phase_changed",
1340
+ from: loaded.state.phase,
1341
+ to: phase,
1342
+ mutationBoundary: loaded.state.mutationBoundary,
1343
+ ...(causeActionId === undefined ? {} : { causeActionId }),
1344
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest }),
1345
+ observedAt: this.timestamp(loaded.state.updatedAt)
1346
+ };
1347
+ try {
1348
+ return await this.journal.append(operationId, loaded.state.revision, event);
1349
+ }
1350
+ catch (error) {
1351
+ if (await this.eventEffectExists(operationId, requestDigest, state => phaseReached(state.phase, phase))) {
1352
+ return await this.journal.load(operationId, requestDigest);
1353
+ }
1354
+ if (isRevisionConflict(error))
1355
+ continue;
1356
+ throw this.serviceError(error, "journal_unavailable");
1357
+ }
1358
+ }
1359
+ throw new OperationServiceError("journal_conflict", "Concurrent operation writers did not converge within the retry bound.");
1360
+ }
1361
+ async appendConvergent(operationId, requestDigest, event, effect) {
1362
+ for (let attempt = 0; attempt < this.maxCasRetries; attempt += 1) {
1363
+ const loaded = await this.journal.load(operationId, requestDigest);
1364
+ if (effect(loaded.state))
1365
+ return loaded;
1366
+ try {
1367
+ return await this.journal.append(operationId, loaded.state.revision, event);
1368
+ }
1369
+ catch (error) {
1370
+ try {
1371
+ const observed = await this.journal.load(operationId, requestDigest);
1372
+ if (effect(observed.state))
1373
+ return observed;
1374
+ }
1375
+ catch {
1376
+ // Preserve the original write error. A committed-prefix recovery
1377
+ // attempt must never turn corruption into a successful mutation.
1378
+ }
1379
+ if (isRevisionConflict(error))
1380
+ continue;
1381
+ throw this.serviceError(error, "journal_unavailable");
1382
+ }
1383
+ }
1384
+ throw new OperationServiceError("journal_conflict", "Concurrent operation writers did not converge within the retry bound.");
1385
+ }
1386
+ async eventEffectExists(operationId, requestDigest, effect) {
1387
+ const loaded = await this.journal.load(operationId, requestDigest);
1388
+ return effect(loaded.state);
1389
+ }
1390
+ async persistProgress(request) {
1391
+ assertOperationStateShape(request.durable.state);
1392
+ assertDigest(request.evidenceDigest, "progress evidenceDigest");
1393
+ const operationId = request.durable.state.operationId;
1394
+ const requestDigest = request.durable.state.requestDigest;
1395
+ let current = await this.journal.load(operationId, requestDigest);
1396
+ const submit = originalSendAction(current.state);
1397
+ if (submit === undefined) {
1398
+ throw new OperationServiceError("operation_state_inconsistent", "Owned progress has no durable original Send intent.");
1399
+ }
1400
+ if (current.state.submissionWitness === undefined) {
1401
+ throw new OperationServiceError("submission_witness_missing", "Owned progress cannot advance without a durable submission witness.");
1402
+ }
1403
+ if (current.state.ownershipBaseline === undefined) {
1404
+ throw new OperationServiceError("ownership_baseline_missing", "Owned progress cannot advance without a durable pre-Send ownership baseline.");
1405
+ }
1406
+ const activeOwnership = latestSubmissionOwnership(current.state);
1407
+ assertCollectorOwnership(request.durable, activeOwnership);
1408
+ if (current.state.phase === "completed" || phaseReached(current.state.phase, request.phase)) {
1409
+ return { ...request.durable, state: current.state };
1410
+ }
1411
+ if (current.state.phase === "prepared" || current.state.phase === "handoff_pending" || current.state.phase === "capturing") {
1412
+ throw new OperationServiceError("operation_state_inconsistent", `Owned progress cannot advance from ${current.state.phase}.`);
1413
+ }
1414
+ // Repair both crash boundaries around Send before advancing observable
1415
+ // generation state. Every append is convergent and browser-free.
1416
+ if (current.state.phase === "ready") {
1417
+ if (submit.outcome === undefined) {
1418
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "send_pending", submit.actionId);
1419
+ }
1420
+ else {
1421
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "uncertain", submit.actionId, request.evidenceDigest);
1422
+ }
1423
+ }
1424
+ if (submit.outcome === undefined) {
1425
+ current = await this.appendActionReceiptConvergent(operationId, requestDigest, submit.actionId, "satisfied", request.evidenceDigest);
1426
+ }
1427
+ else if (submit.outcome !== "satisfied") {
1428
+ throw new OperationServiceError("operation_state_corrupt", "Owned progress conflicts with the durable Send outcome.");
1429
+ }
1430
+ current = await this.journal.load(operationId, requestDigest);
1431
+ if (current.state.phase === "send_pending" || current.state.phase === "uncertain") {
1432
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "submitted", submit.actionId, request.evidenceDigest);
1433
+ }
1434
+ if (request.phase === "generating" && current.state.phase === "submitted") {
1435
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "generating", submit.actionId, request.evidenceDigest);
1436
+ }
1437
+ const fresh = await this.journal.load(operationId, requestDigest);
1438
+ if (!phaseReached(fresh.state.phase, request.phase)) {
1439
+ throw new OperationServiceError("operation_state_inconsistent", "Durable progress did not reach the proven ownership phase.");
1440
+ }
1441
+ return { ...request.durable, state: fresh.state };
1442
+ }
1443
+ async persistTerminal(request, artifactAdapter) {
1444
+ assertOperationStateShape(request.durable.state);
1445
+ const operationId = request.durable.state.operationId;
1446
+ const requestDigest = request.durable.state.requestDigest;
1447
+ validateReceiptIdentity(request.receipt, operationId, requestDigest, request.durable.binding.targetBindingDigest);
1448
+ let current = await this.journal.load(operationId, requestDigest);
1449
+ if (current.state.receipt !== undefined) {
1450
+ const transferCapture = current.state.capturePolicy?.artifacts === "transfer";
1451
+ if (!sameReceipt(current.state.receipt, request.receipt)
1452
+ && !(transferCapture && sameTerminalReceiptIdentity(current.state.receipt, request.receipt))) {
1453
+ throw new OperationServiceError("operation_receipt_indeterminate", "A different terminal receipt is already durable.");
1454
+ }
1455
+ return { ...request.durable, state: current.state };
1456
+ }
1457
+ const submit = originalSendAction(current.state);
1458
+ if (submit === undefined)
1459
+ throw new OperationServiceError("operation_state_inconsistent", "Terminal collection has no durable submission intent.");
1460
+ if (current.state.submissionWitness === undefined) {
1461
+ throw new OperationServiceError("submission_witness_missing", "Terminal collection cannot advance without a durable submission witness.");
1462
+ }
1463
+ if (current.state.ownershipBaseline === undefined) {
1464
+ throw new OperationServiceError("ownership_baseline_missing", "Terminal collection cannot advance without a durable pre-Send ownership baseline.");
1465
+ }
1466
+ const activeOwnership = latestSubmissionOwnership(current.state);
1467
+ assertCollectorOwnership(request.durable, activeOwnership);
1468
+ current = await this.advanceToCapturing(current, submit, request.receipt);
1469
+ // The collector owns the exact terminal observation. Transfer is the one
1470
+ // explicitly requested local effect layered on top of that observation.
1471
+ // It runs only after the operation has durably entered `capturing`, and
1472
+ // every browser/provider capability remains inside the adapter closure.
1473
+ const terminalReceipt = current.state.capturePolicy?.artifacts === "transfer"
1474
+ ? await this.persistTerminalArtifactTransfers(current, request.receipt, artifactAdapter, request.signal, request.deadlineAt)
1475
+ : request.receipt;
1476
+ const observedAt = this.timestamp(request.receipt.completedAt);
1477
+ const receiptEvent = {
1478
+ type: "receipt_completed",
1479
+ receipt: terminalReceipt,
1480
+ observedAt
1481
+ };
1482
+ current = await this.appendConvergent(operationId, requestDigest, receiptEvent, state => state.receipt !== undefined && sameReceipt(state.receipt, terminalReceipt));
1483
+ const fresh = await this.journal.load(operationId, requestDigest);
1484
+ if (fresh.state.phase !== "completed" || fresh.state.receipt === undefined || !sameReceipt(fresh.state.receipt, terminalReceipt)) {
1485
+ throw new OperationServiceError("operation_receipt_indeterminate", "Terminal receipt was not durably validated after persistence.");
1486
+ }
1487
+ return { ...request.durable, state: fresh.state };
1488
+ }
1489
+ /**
1490
+ * Transfer each exact terminal artifact and project only durable transfer
1491
+ * facts into the operation receipt. No provider result is trusted until its
1492
+ * journal callbacks have produced an authenticated artifact-transfer
1493
+ * receipt. A missing adapter, a prior intent without a receipt, or an
1494
+ * adapter protocol failure closes the one-shot obligation as partial/blocked
1495
+ * without re-opening the source.
1496
+ */
1497
+ async persistTerminalArtifactTransfers(initial, observed, artifactAdapter, signal, deadlineAt) {
1498
+ if (observed.artifacts.length === 0)
1499
+ return observed;
1500
+ let current = await this.journal.load(initial.state.operationId, initial.state.requestDigest);
1501
+ const projected = [];
1502
+ for (const artifact of observed.artifacts) {
1503
+ const transferReceipt = await this.persistTerminalArtifactTransfer(current, observed, artifact, artifactAdapter, signal, deadlineAt);
1504
+ projected.push(projectArtifactTransferReceipt(artifact, transferReceipt));
1505
+ current = await this.journal.load(initial.state.operationId, initial.state.requestDigest);
1506
+ }
1507
+ const terminalReceipt = {
1508
+ ...observed,
1509
+ artifacts: projected
1510
+ };
1511
+ // This is intentionally a narrow invariant check rather than a call into
1512
+ // collector internals: terminal persistence may enrich artifacts but may
1513
+ // not rewrite the owned turn, response, or artifact identity.
1514
+ assertTerminalArtifactProjection(terminalReceipt, observed);
1515
+ return terminalReceipt;
1516
+ }
1517
+ async persistTerminalArtifactTransfer(current, observed, artifact, artifactAdapter, signal, deadlineAt) {
1518
+ const operationId = current.state.operationId;
1519
+ const requestDigest = current.state.requestDigest;
1520
+ const targetBindingDigest = observed.targetBindingDigest;
1521
+ const existing = findArtifactTransfer(current.state, observed.assistantTurnId, artifact);
1522
+ const transferActionId = this.artifactTransferActionId(operationId, requestDigest, observed.assistantTurnId, artifact);
1523
+ const lookupIdentity = {
1524
+ operationId,
1525
+ requestDigest,
1526
+ targetBindingDigest,
1527
+ assistantTurnId: observed.assistantTurnId,
1528
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
1529
+ kind: artifact.kind,
1530
+ ordinal: artifact.ordinal,
1531
+ transferActionId
1532
+ };
1533
+ const journal = this.artifactTransferJournal(lookupIdentity);
1534
+ const flightKey = artifactTransferFlightKey(lookupIdentity);
1535
+ // If this service already owns the same transfer call, wait for that
1536
+ // exact invocation before interpreting an intent-only prefix. Otherwise a
1537
+ // concurrent collector could prematurely close the other's in-flight
1538
+ // source as partial and make the first receipt append fail.
1539
+ const existingFlight = this.artifactTransfersInFlight.get(flightKey);
1540
+ if (existingFlight !== undefined) {
1541
+ await existingFlight;
1542
+ current = await this.journal.load(operationId, requestDigest);
1543
+ const converged = findArtifactTransfer(current.state, observed.assistantTurnId, artifact);
1544
+ if (converged?.receipt !== undefined)
1545
+ return converged.receipt;
1546
+ if (converged?.intent !== undefined) {
1547
+ return await this.closeArtifactTransferWithoutSource(current, converged.intent, "artifact_transfer_partial");
1548
+ }
1549
+ }
1550
+ if (existing?.receipt !== undefined)
1551
+ return existing.receipt;
1552
+ // A durable intent is already the source boundary. The transfer primitive
1553
+ // must never retry the provider after a restart or an acknowledgement
1554
+ // fault; close it with a path-free partial receipt instead.
1555
+ if (existing?.intent !== undefined) {
1556
+ return await this.closeArtifactTransferWithoutSource(current, existing.intent, "artifact_transfer_partial");
1557
+ }
1558
+ let result;
1559
+ if (artifactAdapter !== undefined) {
1560
+ const transferRequest = Object.freeze({
1561
+ ...lookupIdentity,
1562
+ ...(artifact.mimeType === undefined ? {} : { mimeTypeHint: artifact.mimeType }),
1563
+ signal,
1564
+ deadlineAt,
1565
+ journal
1566
+ });
1567
+ const flight = this.invokeArtifactAdapter(artifactAdapter, transferRequest);
1568
+ this.artifactTransfersInFlight.set(flightKey, flight);
1569
+ try {
1570
+ result = await flight;
1571
+ }
1572
+ finally {
1573
+ if (this.artifactTransfersInFlight.get(flightKey) === flight) {
1574
+ this.artifactTransfersInFlight.delete(flightKey);
1575
+ }
1576
+ }
1577
+ }
1578
+ // Always authenticate the adapter's durable side effects after it returns.
1579
+ // The result object is useful only as a diagnostic signal; its receipt is
1580
+ // not an authority until it is present in the operation journal.
1581
+ current = await this.journal.load(operationId, requestDigest);
1582
+ const after = findArtifactTransfer(current.state, observed.assistantTurnId, artifact);
1583
+ if (after?.receipt !== undefined)
1584
+ return after.receipt;
1585
+ if (after?.intent !== undefined) {
1586
+ return await this.closeArtifactTransferWithoutSource(current, after.intent, artifactAdapter === undefined || result === undefined
1587
+ ? "artifact_transfer_unavailable"
1588
+ : "artifact_transfer_protocol_violation");
1589
+ }
1590
+ // The adapter may have thrown before it could persist its destination
1591
+ // identity. Establish a deterministic, path-free durable boundary and
1592
+ // close it as blocked. This also handles a missing restart adapter.
1593
+ const fallbackIntent = this.makeUnavailableArtifactTransferIntent(current, observed.assistantTurnId, artifact, transferActionId);
1594
+ await this.persistArtifactTransferIntent(fallbackIntent);
1595
+ const withIntent = await this.journal.load(operationId, requestDigest);
1596
+ return await this.closeArtifactTransferWithoutSource(withIntent, fallbackIntent, artifactAdapter === undefined ? "artifact_transfer_unavailable" : "artifact_transfer_protocol_violation", "blocked");
1597
+ }
1598
+ async invokeArtifactAdapter(adapter, request) {
1599
+ try {
1600
+ const result = await adapter.transfer(request);
1601
+ if (result === null || typeof result !== "object") {
1602
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer adapter returned an invalid result.");
1603
+ }
1604
+ return result;
1605
+ }
1606
+ catch {
1607
+ // Do not carry adapter/provider diagnostics over the service boundary.
1608
+ // The caller reconciles the journal and closes the obligation safely.
1609
+ return {
1610
+ schemaVersion: "chatgpt.browser_control.artifact_transfer.v1",
1611
+ outcome: "uncertain",
1612
+ replayed: false,
1613
+ intentPersistence: "indeterminate",
1614
+ receiptPersistence: "indeterminate",
1615
+ blockerCode: "artifact_transfer_exception"
1616
+ };
1617
+ }
1618
+ }
1619
+ artifactTransferActionId(operationId, requestDigest, assistantTurnId, artifact) {
1620
+ const evidence = this.journal.evidenceDigest("artifact-transfer-action", {
1621
+ operationId,
1622
+ requestDigest,
1623
+ assistantTurnId,
1624
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
1625
+ kind: artifact.kind,
1626
+ ordinal: artifact.ordinal
1627
+ });
1628
+ const hex = evidence.slice("hmac-sha256:".length);
1629
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
1630
+ }
1631
+ makeUnavailableArtifactTransferIntent(current, assistantTurnId, artifact, transferActionId) {
1632
+ const destinationIdentityDigest = this.journal.evidenceDigest("artifact-destination", {
1633
+ schemaVersion: OPERATION_ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION,
1634
+ operationId: current.state.operationId,
1635
+ requestDigest: current.state.requestDigest,
1636
+ targetBindingDigest: this.targetBindingDigest(current.state),
1637
+ assistantTurnId,
1638
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
1639
+ kind: artifact.kind,
1640
+ ordinal: artifact.ordinal,
1641
+ transferActionId,
1642
+ destination: "unavailable"
1643
+ });
1644
+ return {
1645
+ schemaVersion: OPERATION_ARTIFACT_TRANSFER_INTENT_SCHEMA_VERSION,
1646
+ operationId: current.state.operationId,
1647
+ requestDigest: current.state.requestDigest,
1648
+ targetBindingDigest: this.targetBindingDigest(current.state),
1649
+ assistantTurnId,
1650
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
1651
+ kind: artifact.kind,
1652
+ ordinal: artifact.ordinal,
1653
+ transferActionId,
1654
+ destinationIdentityDigest,
1655
+ actionKind: "local_output_commit",
1656
+ repeatPolicy: "reconcile_local_effect",
1657
+ intentAt: current.state.updatedAt
1658
+ };
1659
+ }
1660
+ async closeArtifactTransferWithoutSource(current, intent, blockerCode, status = "partial") {
1661
+ const receipt = {
1662
+ schemaVersion: OPERATION_ARTIFACT_TRANSFER_RECEIPT_SCHEMA_VERSION,
1663
+ operationId: intent.operationId,
1664
+ requestDigest: intent.requestDigest,
1665
+ targetBindingDigest: intent.targetBindingDigest,
1666
+ assistantTurnId: intent.assistantTurnId,
1667
+ sourceIdentityDigest: intent.sourceIdentityDigest,
1668
+ kind: intent.kind,
1669
+ ordinal: intent.ordinal,
1670
+ transferActionId: intent.transferActionId,
1671
+ destinationIdentityDigest: intent.destinationIdentityDigest,
1672
+ status,
1673
+ blockerCode,
1674
+ observedAt: this.timestamp(intent.intentAt)
1675
+ };
1676
+ await this.persistArtifactTransferReceipt(receipt);
1677
+ const fresh = await this.journal.load(current.state.operationId, current.state.requestDigest);
1678
+ const persisted = findArtifactTransfer(fresh.state, intent.assistantTurnId, {
1679
+ operationId: intent.operationId,
1680
+ assistantTurnId: intent.assistantTurnId,
1681
+ sourceIdentityDigest: intent.sourceIdentityDigest,
1682
+ kind: intent.kind,
1683
+ ordinal: intent.ordinal
1684
+ });
1685
+ if (persisted?.receipt === undefined) {
1686
+ throw new OperationServiceError("operation_receipt_indeterminate", "Artifact transfer receipt was not durably validated.");
1687
+ }
1688
+ return persisted.receipt;
1689
+ }
1690
+ artifactTransferJournal(expected) {
1691
+ // The adapter receives a service-owned port, not the whole operation
1692
+ // journal. Bind every callback to this exact artifact tuple so an adapter
1693
+ // cannot accidentally (or maliciously) append a sibling transfer for a
1694
+ // different operation, turn, source, kind, ordinal, or action ID. The
1695
+ // destination identity is intentionally left to the adapter: it is
1696
+ // derived from the request-local destination and is checked by the
1697
+ // durable state machine when the callback is appended.
1698
+ const assertExpectedIdentity = (actual) => {
1699
+ if (!sameArtifactTransferIdentity(expected, actual)) {
1700
+ throw new OperationServiceError("operation_request_mismatch", "Artifact transfer callback identity does not match the exact terminal artifact.");
1701
+ }
1702
+ };
1703
+ return Object.freeze({
1704
+ readActionState: (lookup) => {
1705
+ assertExpectedIdentity(lookup);
1706
+ return this.readArtifactTransferState(lookup);
1707
+ },
1708
+ persistIntent: (intent) => {
1709
+ assertExpectedIdentity(intent);
1710
+ return this.persistArtifactTransferIntent(intent);
1711
+ },
1712
+ persistReceipt: (receipt) => {
1713
+ assertExpectedIdentity(receipt);
1714
+ return this.persistArtifactTransferReceipt(receipt);
1715
+ }
1716
+ });
1717
+ }
1718
+ async readArtifactTransferState(lookup) {
1719
+ const current = await this.journal.load(lookup.operationId, lookup.requestDigest);
1720
+ if (current.state.target === undefined || this.targetBindingDigest(current.state) !== lookup.targetBindingDigest) {
1721
+ throw new OperationServiceError("operation_request_mismatch", "Artifact transfer target identity does not match durable state.");
1722
+ }
1723
+ const transfer = current.state.artifactTransfers?.[lookup.transferActionId];
1724
+ if (transfer === undefined)
1725
+ return undefined;
1726
+ if (transfer.intent === undefined) {
1727
+ throw new OperationServiceError("operation_state_corrupt", "Artifact transfer state is missing its durable intent.");
1728
+ }
1729
+ if (!artifactTransferIdentityMatches(transfer.intent, lookup)) {
1730
+ throw new OperationServiceError("operation_request_mismatch", "Artifact transfer action identity does not match durable state.");
1731
+ }
1732
+ return transfer;
1733
+ }
1734
+ async persistArtifactTransferIntent(intent) {
1735
+ const event = {
1736
+ type: "artifact_transfer_intent",
1737
+ intent
1738
+ };
1739
+ await this.appendConvergent(intent.operationId, intent.requestDigest, event, state => {
1740
+ const transfer = state.artifactTransfers?.[intent.transferActionId];
1741
+ return transfer?.intent !== undefined && canonicalJson(transfer.intent) === canonicalJson(intent);
1742
+ });
1743
+ }
1744
+ async persistArtifactTransferReceipt(receipt) {
1745
+ const event = {
1746
+ type: "artifact_transfer_receipt",
1747
+ receipt
1748
+ };
1749
+ await this.appendConvergent(receipt.operationId, receipt.requestDigest, event, state => state.artifactTransfers?.[receipt.transferActionId]?.receipt !== undefined
1750
+ && canonicalJson(state.artifactTransfers[receipt.transferActionId].receipt) === canonicalJson(receipt));
1751
+ }
1752
+ async advanceToCapturing(initial, submit, receipt) {
1753
+ const operationId = initial.state.operationId;
1754
+ const requestDigest = initial.state.requestDigest;
1755
+ let current = initial;
1756
+ if (current.state.phase === "completed")
1757
+ return current;
1758
+ if (current.state.phase === "prepared" || current.state.phase === "handoff_pending") {
1759
+ throw new OperationServiceError("operation_state_inconsistent", "Terminal ownership cannot precede a durable Send intent phase.");
1760
+ }
1761
+ // Crash gap: the Send intent append can be durable while the adjacent
1762
+ // send_pending phase append is absent. Preserve legal causality before
1763
+ // recording the observed user-turn receipt.
1764
+ if (current.state.phase === "ready") {
1765
+ if (submit.outcome === undefined) {
1766
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "send_pending", submit.actionId);
1767
+ }
1768
+ else {
1769
+ // A satisfied receipt at ready is a valid committed prefix but cannot
1770
+ // cross ready -> submitted directly. Quarantine, then recover through
1771
+ // the state machine's evidence-gated uncertain edge.
1772
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "uncertain", submit.actionId, receipt.userTurnEvidenceDigest);
1773
+ }
1774
+ }
1775
+ if (submit.outcome === undefined) {
1776
+ current = await this.appendActionReceiptConvergent(operationId, requestDigest, submit.actionId, "satisfied", receipt.userTurnEvidenceDigest);
1777
+ }
1778
+ else if (submit.outcome !== "satisfied") {
1779
+ throw new OperationServiceError("operation_state_corrupt", "Terminal ownership conflicts with the durable Send outcome.");
1780
+ }
1781
+ current = await this.journal.load(operationId, requestDigest);
1782
+ if (current.state.phase === "send_pending" || current.state.phase === "uncertain") {
1783
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "submitted", submit.actionId, receipt.userTurnEvidenceDigest);
1784
+ }
1785
+ if (current.state.phase === "submitted" || current.state.phase === "generating" || current.state.phase === "uncertain") {
1786
+ current = await this.appendPhaseConvergent(operationId, requestDigest, "capturing", submit.actionId, receipt.ownershipEvidenceDigest);
1787
+ }
1788
+ if (current.state.phase !== "capturing" && current.state.phase !== "completed") {
1789
+ throw new OperationServiceError("operation_state_inconsistent", `Terminal receipt cannot advance from ${current.state.phase}.`);
1790
+ }
1791
+ return current;
1792
+ }
1793
+ /**
1794
+ * Rebuild the prompt-free Work-steer prepared record from authenticated
1795
+ * journal state. The journal stores the action-prepared action/baseline;
1796
+ * assistant branch identity is derived from that baseline and the caller's
1797
+ * exact control request, then the keyed prepared digest is recomputed.
1798
+ */
1799
+ reconstructSteerIntent(state, controlActionId, parentRequestDigest, parentTargetBindingDigest, requestDigest, expectedAssistantTurnId) {
1800
+ const action = state.actions[controlActionId];
1801
+ if (action === undefined
1802
+ || action.kind !== "work_steer"
1803
+ || action.requestDigest !== requestDigest
1804
+ || action.targetDigest !== parentTargetBindingDigest) {
1805
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer action identity is invalid.");
1806
+ }
1807
+ const durableBaseline = state.ownershipBaselines?.[controlActionId];
1808
+ if (durableBaseline === undefined
1809
+ || durableBaseline.operationId !== state.operationId
1810
+ || durableBaseline.requestDigest !== parentRequestDigest
1811
+ || durableBaseline.targetBindingDigest !== parentTargetBindingDigest
1812
+ || durableBaseline.actionId !== controlActionId) {
1813
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer action is missing its complete per-action baseline.");
1814
+ }
1815
+ const assistants = durableBaseline.baseline.assistantTurns.filter(assistant => assistant.stableId === expectedAssistantTurnId);
1816
+ if (assistants.length !== 1) {
1817
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer baseline does not identify exactly one expected assistant turn.");
1818
+ }
1819
+ const assistant = assistants[0];
1820
+ if (assistant.branchStableId === undefined || assistant.parentStableId === undefined) {
1821
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer baseline is missing assistant branch ancestry.");
1822
+ }
1823
+ let material;
1824
+ try {
1825
+ material = controlSteerPreparedDigestMaterial({
1826
+ parentOperationId: state.operationId,
1827
+ parentRequestDigest,
1828
+ parentTargetBindingDigest,
1829
+ controlActionId,
1830
+ expectedAssistantTurnId,
1831
+ assistantBranchId: assistant.branchStableId,
1832
+ assistantParentTurnId: assistant.parentStableId,
1833
+ baselineSnapshotDigest: durableBaseline.baseline.snapshotDigest,
1834
+ baseline: durableBaseline.baseline
1835
+ });
1836
+ }
1837
+ catch {
1838
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer prepared material is invalid.");
1839
+ }
1840
+ const preparedDigest = this.journal.evidenceDigest("work-steer-prepared", material);
1841
+ return Object.freeze({
1842
+ schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
1843
+ parentOperationId: state.operationId,
1844
+ parentRequestDigest,
1845
+ parentTargetBindingDigest,
1846
+ controlActionId,
1847
+ action: "steer",
1848
+ requestDigest,
1849
+ expectedAssistantTurnId,
1850
+ assistantBranchId: assistant.branchStableId,
1851
+ assistantParentTurnId: assistant.parentStableId,
1852
+ baselineSnapshotDigest: durableBaseline.baseline.snapshotDigest,
1853
+ preparedDigest,
1854
+ baseline: material.baseline
1855
+ });
1856
+ }
1857
+ async readControlParent(request) {
1858
+ const loaded = await this.journal.load(request.operationId, request.parentRequestDigest);
1859
+ const handle = this.journal.handleFromState(loaded.state);
1860
+ this.journal.validateHandle({
1861
+ ...handle,
1862
+ schemaVersion: OPERATION_HANDLE_SCHEMA_VERSION,
1863
+ operationId: request.operationId,
1864
+ requestDigest: request.parentRequestDigest,
1865
+ surface: loaded.state.surface,
1866
+ revision: loaded.state.revision,
1867
+ phase: loaded.state.phase,
1868
+ mutationBoundary: loaded.state.mutationBoundary,
1869
+ ...(request.parentTargetBindingDigest === undefined ? {} : { targetBindingDigest: request.parentTargetBindingDigest })
1870
+ }, loaded.state);
1871
+ if (loaded.state.target?.targetLifecycle === "new_pending") {
1872
+ throw new OperationServiceError("target_establishment_required", "Control requires durable provider identity establishment for a new target.");
1873
+ }
1874
+ if (request.action === "steer" && loaded.state.surface !== "work") {
1875
+ throw new OperationServiceError("operation_request_mismatch", "Work steer requires a durable Work operation.");
1876
+ }
1877
+ if (request.action === "steer") {
1878
+ // A satisfied Work action without its authenticated baseline+witness is
1879
+ // a corrupt prefix, not an invitation to prepare another provider read.
1880
+ // Quarantine it at the parent-read boundary so a fresh caller cannot
1881
+ // even invoke the adapter's preparation port before the inconsistency
1882
+ // is surfaced.
1883
+ for (const prior of Object.values(loaded.state.actions)) {
1884
+ if (prior.kind === "work_steer"
1885
+ && (prior.outcome === "satisfied" || prior.outcome === "not_satisfied")) {
1886
+ priorWorkSteerDisposition(loaded.state, prior);
1887
+ }
1888
+ }
1889
+ }
1890
+ const action = loaded.state.actions[request.controlActionId];
1891
+ if (action !== undefined && (action.kind !== (request.action === "steer" ? "work_steer" : "stop")
1892
+ || action.requestDigest !== request.requestDigest
1893
+ || action.targetDigest !== request.parentTargetBindingDigest)) {
1894
+ throw new OperationServiceError("operation_request_mismatch", "Control action identity conflicts with durable state.");
1895
+ }
1896
+ let existingSteerIntent;
1897
+ if (request.action === "steer" && action !== undefined) {
1898
+ const witness = submissionWitnessForAction(loaded.state, action);
1899
+ if (action.outcome === "satisfied" && witness === undefined) {
1900
+ // A generic satisfied action without its causal Work witness is an
1901
+ // impossible/unsafe prefix. Never let a replay interpret it as a
1902
+ // completed control or invoke a browser mutation again.
1903
+ throw new OperationServiceError("operation_state_corrupt", "Satisfied Work-steer action is missing its durable submission witness.");
1904
+ }
1905
+ existingSteerIntent = this.reconstructSteerIntent(loaded.state, action.actionId, request.parentRequestDigest, request.parentTargetBindingDigest, request.requestDigest, request.expectedAssistantTurnId);
1906
+ if (witness !== undefined) {
1907
+ const baseline = loaded.state.ownershipBaselines?.[action.actionId];
1908
+ if (baseline === undefined
1909
+ || witness.actionKind !== "work_steer"
1910
+ || witness.targetBindingDigest !== request.parentTargetBindingDigest
1911
+ || witness.baselineSnapshotDigest !== baseline.baseline.snapshotDigest) {
1912
+ throw new OperationServiceError("operation_state_corrupt", "Durable Work-steer witness does not match its prepared baseline.");
1913
+ }
1914
+ }
1915
+ }
1916
+ const existingReceipt = action?.outcome === undefined || action === undefined
1917
+ ? undefined
1918
+ : controlReceiptFromAction(request, action, action.receiptAt);
1919
+ return {
1920
+ state: loaded.state,
1921
+ handle,
1922
+ ...(existingReceipt === undefined ? {} : { existingReceipt }),
1923
+ ...(existingSteerIntent === undefined ? {} : { existingSteerIntent })
1924
+ };
1925
+ }
1926
+ async persistControlIntent(request) {
1927
+ const parent = await this.journal.load(request.operationId, request.parentRequestDigest);
1928
+ await this.persistActionIntent({
1929
+ operationId: request.operationId,
1930
+ requestDigest: request.requestDigest,
1931
+ durableRequestDigest: request.parentRequestDigest,
1932
+ surface: parent.state.surface,
1933
+ actionId: request.controlActionId,
1934
+ kind: request.action === "steer" ? "work_steer" : "stop",
1935
+ repeatPolicy: "observe_only_after_intent",
1936
+ targetBindingDigest: request.targetBindingDigest
1937
+ });
1938
+ }
1939
+ async persistControlReceipt(request) {
1940
+ const receipt = request.receipt;
1941
+ if (receipt.schemaVersion !== OPERATION_CONTROL_RECEIPT_SCHEMA_VERSION
1942
+ || receipt.action !== "stop" && receipt.action !== "steer"
1943
+ || !UUID_PATTERN.test(receipt.parentOperationId)
1944
+ || !DIGEST_PATTERN.test(receipt.parentRequestDigest)
1945
+ || !DIGEST_PATTERN.test(receipt.parentTargetBindingDigest)
1946
+ || !DIGEST_PATTERN.test(receipt.requestDigest)
1947
+ || !UUID_PATTERN.test(receipt.controlActionId)
1948
+ || !isSafeOpaqueId(receipt.expectedAssistantTurnId)
1949
+ || !isCanonicalTimestamp(receipt.observedAt)) {
1950
+ throw new OperationServiceError("operation_state_corrupt", "Control receipt identity is invalid.");
1951
+ }
1952
+ const current = await this.journal.load(receipt.parentOperationId, receipt.parentRequestDigest);
1953
+ const action = current.state.actions[receipt.controlActionId];
1954
+ if (action === undefined
1955
+ || action.kind !== (receipt.action === "steer" ? "work_steer" : "stop")
1956
+ || action.requestDigest !== receipt.requestDigest
1957
+ || action.targetDigest !== receipt.parentTargetBindingDigest) {
1958
+ throw new OperationServiceError("operation_request_mismatch", "Control receipt does not match its durable action identity.");
1959
+ }
1960
+ if (receipt.action === "steer") {
1961
+ if (receipt.outcome !== "satisfied") {
1962
+ if (request.steerReceipt !== undefined) {
1963
+ throw new OperationServiceError("operation_state_corrupt", "A non-satisfied Work-steer receipt cannot carry satisfied witness evidence.");
1964
+ }
1965
+ }
1966
+ else {
1967
+ if (request.steerReceipt === undefined) {
1968
+ throw new OperationServiceError("submission_witness_missing", "A satisfied Work-steer receipt requires its rich causal witness.");
1969
+ }
1970
+ const prepared = this.reconstructSteerIntent(current.state, action.actionId, receipt.parentRequestDigest, receipt.parentTargetBindingDigest, receipt.requestDigest, receipt.expectedAssistantTurnId);
1971
+ validateSteerVerificationReceipt(request.steerReceipt, prepared, receipt);
1972
+ const witness = {
1973
+ schemaVersion: OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION,
1974
+ actionId: action.actionId,
1975
+ actionKind: "work_steer",
1976
+ targetBindingDigest: receipt.parentTargetBindingDigest,
1977
+ baselineSnapshotDigest: request.steerReceipt.baselineSnapshotDigest,
1978
+ postSendDeltaDigest: request.steerReceipt.postSendDeltaDigest,
1979
+ operationUserEvidenceDigest: request.steerReceipt.userTurnEvidenceDigest,
1980
+ userTurnId: request.steerReceipt.userTurnId,
1981
+ observedAt: receipt.observedAt
1982
+ };
1983
+ // Causal ownership must be durable before the generic action receipt;
1984
+ // a crash after this append cannot leave a satisfied steer without
1985
+ // the witness needed to classify its output.
1986
+ await this.appendSubmissionWitnessConvergent(receipt.parentOperationId, receipt.parentRequestDigest, witness);
1987
+ }
1988
+ }
1989
+ else if (request.steerReceipt !== undefined) {
1990
+ throw new OperationServiceError("operation_state_corrupt", "Stop receipts cannot carry Work-steer witness evidence.");
1991
+ }
1992
+ await this.appendActionReceiptConvergent(receipt.parentOperationId, receipt.parentRequestDigest, receipt.controlActionId, receipt.outcome, receipt.evidenceDigest, receipt.blockerCode, receipt.observedAt);
1993
+ const after = await this.journal.load(receipt.parentOperationId, receipt.parentRequestDigest);
1994
+ if (after.state.actions[receipt.controlActionId]?.outcome !== receipt.outcome) {
1995
+ throw new OperationServiceError("journal_unavailable", "Control receipt was not durably validated.");
1996
+ }
1997
+ }
1998
+ async loadForHandle(handle) {
1999
+ if (!handle || handle.schemaVersion !== OPERATION_HANDLE_SCHEMA_VERSION) {
2000
+ throw new OperationServiceError("invalid_operation_handle", "Operation handle schema is unsupported.");
2001
+ }
2002
+ let loaded;
2003
+ try {
2004
+ loaded = await this.journal.load(handle.operationId, handle.requestDigest);
2005
+ this.journal.validateHandle(handle, loaded.state);
2006
+ }
2007
+ catch (error) {
2008
+ throw this.serviceError(error, "invalid_operation_handle");
2009
+ }
2010
+ return loaded;
2011
+ }
2012
+ targetBindingDigest(state) {
2013
+ const digest = this.journal.handleFromState(state).targetBindingDigest;
2014
+ if (digest === undefined)
2015
+ throw new OperationServiceError("target_binding_missing", "Operation has no durable target binding.");
2016
+ return digest;
2017
+ }
2018
+ timestamp(notBefore) {
2019
+ const value = this.now();
2020
+ if (!Number.isFinite(value))
2021
+ throw new OperationServiceError("invalid_clock", "Operation service clock returned a non-finite value.");
2022
+ if (notBefore === undefined)
2023
+ return new Date(value).toISOString();
2024
+ const floor = Date.parse(notBefore);
2025
+ if (!Number.isFinite(floor))
2026
+ throw new OperationServiceError("invalid_timestamp", "Durable state timestamp is invalid.");
2027
+ return new Date(Math.max(value, floor)).toISOString();
2028
+ }
2029
+ serviceError(error, fallback) {
2030
+ const fallbackCode = safeServiceErrorCode(fallback) ?? "journal_unavailable";
2031
+ const code = safeServiceErrorCode(readOwnErrorCode(error)) ?? fallbackCode;
2032
+ const message = SAFE_SERVICE_ERROR_MESSAGES[code]
2033
+ ?? SAFE_SERVICE_ERROR_MESSAGES[fallbackCode]
2034
+ ?? "The operation failed safely.";
2035
+ return new OperationServiceError(code, message);
2036
+ }
2037
+ }
2038
+ function readOwnErrorCode(error) {
2039
+ if (!(error instanceof OperationServiceError) && !(error instanceof OperationJournalError))
2040
+ return undefined;
2041
+ try {
2042
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
2043
+ return descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string"
2044
+ ? descriptor.value
2045
+ : undefined;
2046
+ }
2047
+ catch {
2048
+ return undefined;
2049
+ }
2050
+ }
2051
+ function safeServiceErrorCode(code) {
2052
+ return code !== undefined
2053
+ && code !== "default"
2054
+ && Object.prototype.hasOwnProperty.call(SAFE_SERVICE_ERROR_MESSAGES, code)
2055
+ ? code
2056
+ : undefined;
2057
+ }
2058
+ function uniqueAction(state, kind) {
2059
+ const matches = Object.values(state.actions).filter(action => action.kind === kind);
2060
+ if (matches.length > 1)
2061
+ throw new OperationServiceError("operation_state_corrupt", `Operation contains duplicate ${kind} actions.`);
2062
+ return matches[0];
2063
+ }
2064
+ function originalSendAction(state) {
2065
+ return uniqueAction(state, "send");
2066
+ }
2067
+ function submissionWitnessForAction(state, action) {
2068
+ const mapped = state.submissionWitnesses?.[action.actionId];
2069
+ if (mapped !== undefined)
2070
+ return mapped;
2071
+ if (action.kind === "send" && state.submissionWitness?.actionId === action.actionId) {
2072
+ return state.submissionWitness;
2073
+ }
2074
+ return undefined;
2075
+ }
2076
+ /**
2077
+ * A Work steer may be followed by another caller-owned steer only after its
2078
+ * outcome is durably settled. Satisfied actions require the complete causal
2079
+ * baseline+witness pair; a clean not_satisfied action is safe to ignore and
2080
+ * intentionally has no witness. Missing or mismatched evidence on a
2081
+ * satisfied action is corruption, never permission to continue.
2082
+ */
2083
+ function priorWorkSteerDisposition(state, action) {
2084
+ if (action.kind !== "work_steer") {
2085
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer disposition was requested for a non-steer action.");
2086
+ }
2087
+ if (action.outcome === undefined || action.outcome === "uncertain")
2088
+ return "unresolved";
2089
+ if (action.outcome === "not_satisfied") {
2090
+ if (submissionWitnessForAction(state, action) !== undefined) {
2091
+ throw new OperationServiceError("operation_state_corrupt", "A rejected Work-steer action cannot carry a submission witness.");
2092
+ }
2093
+ return "settled";
2094
+ }
2095
+ const baseline = state.ownershipBaselines?.[action.actionId];
2096
+ const witness = submissionWitnessForAction(state, action);
2097
+ if (baseline === undefined || witness === undefined) {
2098
+ throw new OperationServiceError("operation_state_corrupt", "Satisfied Work-steer action is missing its durable ownership pair.");
2099
+ }
2100
+ if (baseline.operationId !== state.operationId
2101
+ || baseline.requestDigest !== state.requestDigest
2102
+ || baseline.actionId !== action.actionId
2103
+ || baseline.targetBindingDigest !== action.targetDigest
2104
+ || witness.actionId !== action.actionId
2105
+ || witness.actionKind !== "work_steer"
2106
+ || witness.targetBindingDigest !== action.targetDigest
2107
+ || witness.baselineSnapshotDigest !== baseline.baseline.snapshotDigest) {
2108
+ throw new OperationServiceError("operation_state_corrupt", "Satisfied Work-steer ownership evidence does not match its action.");
2109
+ }
2110
+ return "settled";
2111
+ }
2112
+ function assertCollectorOwnership(durable, active) {
2113
+ if (active === undefined || durable.submissionWitness === undefined) {
2114
+ throw new OperationServiceError("submission_witness_missing", "Collector persistence requires the authenticated causal ownership pair.");
2115
+ }
2116
+ const projected = ownershipWitnessFromDurable(active.witness);
2117
+ const supplied = durable.submissionWitness;
2118
+ if (supplied.actionId !== projected.actionId
2119
+ || supplied.actionKind !== projected.actionKind
2120
+ || supplied.baselineSnapshotDigest !== projected.baselineSnapshotDigest
2121
+ || supplied.postSendDeltaDigest !== projected.postSendDeltaDigest
2122
+ || supplied.operationUserEvidenceDigest !== projected.operationUserEvidenceDigest
2123
+ || supplied.userTurnStableId !== projected.userTurnStableId
2124
+ || canonicalJson(durable.baseline) !== canonicalJson(active.baseline.baseline)) {
2125
+ throw new OperationServiceError("operation_state_corrupt", "Collector persistence ownership does not match the latest authenticated causal action.");
2126
+ }
2127
+ }
2128
+ /**
2129
+ * Select the latest non-rejected causal action that has a complete
2130
+ * authenticated ownership pair. The original Send remains the operation's
2131
+ * completion cause, but a Work steer can supersede it for turn
2132
+ * classification—even in the crash window after its witness is durable and
2133
+ * before its generic action receipt—so the steer's user turn/output delta is
2134
+ * not mistaken for a concurrent user turn. Missing evidence on a candidate is
2135
+ * corruption, not a reason to fall back to an older action.
2136
+ */
2137
+ function latestSubmissionOwnership(state) {
2138
+ // A Work steer that has crossed its durable fence but has not yet produced
2139
+ // its witness is an unresolved ownership boundary. Falling back to the
2140
+ // original Send here could classify a post-steer turn against the wrong
2141
+ // baseline, so quarantine the collector until the exact Work pair is
2142
+ // available. A rejected steer is safe to ignore because it crossed no
2143
+ // satisfied output boundary.
2144
+ for (const action of Object.values(state.actions)) {
2145
+ if (action.kind !== "work_steer" || action.outcome === "not_satisfied")
2146
+ continue;
2147
+ const baseline = state.ownershipBaselines?.[action.actionId];
2148
+ const witness = state.submissionWitnesses?.[action.actionId];
2149
+ if (baseline === undefined || witness === undefined) {
2150
+ throw new OperationServiceError("submission_witness_missing", "A fenced Work-steer action is missing its complete causal ownership evidence.");
2151
+ }
2152
+ }
2153
+ const candidates = Object.values(state.actions)
2154
+ .filter((action) => (action.kind === "send" || action.kind === "work_steer")
2155
+ && action.outcome !== "not_satisfied"
2156
+ && action.outcome !== "uncertain");
2157
+ if (candidates.length === 0)
2158
+ return undefined;
2159
+ const ownershipBaselines = state.ownershipBaselines;
2160
+ const submissionWitnesses = state.submissionWitnesses;
2161
+ const resolved = [];
2162
+ for (const action of candidates) {
2163
+ const baseline = ownershipBaselines?.[action.actionId]
2164
+ ?? (action.kind === "send" && state.ownershipBaseline?.actionId === action.actionId
2165
+ ? state.ownershipBaseline
2166
+ : undefined);
2167
+ const witness = submissionWitnesses?.[action.actionId]
2168
+ ?? (action.kind === "send" && state.submissionWitness?.actionId === action.actionId
2169
+ ? state.submissionWitness
2170
+ : undefined);
2171
+ if (baseline === undefined || witness === undefined) {
2172
+ throw new OperationServiceError("submission_witness_missing", `Satisfied ${action.kind} action is missing its durable ownership evidence.`);
2173
+ }
2174
+ if (baseline.actionId !== action.actionId
2175
+ || baseline.targetBindingDigest !== action.targetDigest
2176
+ || witness.actionId !== action.actionId
2177
+ || witness.actionKind !== action.kind
2178
+ || witness.targetBindingDigest !== action.targetDigest
2179
+ || witness.baselineSnapshotDigest !== baseline.baseline.snapshotDigest) {
2180
+ throw new OperationServiceError("operation_state_corrupt", `Durable ownership evidence does not match satisfied ${action.kind} action.`);
2181
+ }
2182
+ resolved.push({ action, baseline, witness });
2183
+ }
2184
+ resolved.sort((left, right) => {
2185
+ const leftRevision = left.action.receiptRevision ?? left.action.intentRevision;
2186
+ const rightRevision = right.action.receiptRevision ?? right.action.intentRevision;
2187
+ if (leftRevision !== rightRevision)
2188
+ return rightRevision - leftRevision;
2189
+ return left.action.actionId < right.action.actionId ? 1 : left.action.actionId > right.action.actionId ? -1 : 0;
2190
+ });
2191
+ return resolved[0];
2192
+ }
2193
+ function ownershipWitnessFromDurable(witness) {
2194
+ return {
2195
+ actionId: witness.actionId,
2196
+ actionKind: witness.actionKind,
2197
+ baselineSnapshotDigest: witness.baselineSnapshotDigest,
2198
+ postSendDeltaDigest: witness.postSendDeltaDigest,
2199
+ operationUserEvidenceDigest: witness.operationUserEvidenceDigest,
2200
+ ...(witness.userTurnId === undefined ? {} : { userTurnStableId: witness.userTurnId })
2201
+ };
2202
+ }
2203
+ function submissionWitnessMatchesReceipt(witness, receipt, actionId, baselineSnapshotDigest) {
2204
+ return witness.actionId === actionId
2205
+ && witness.actionKind === "send"
2206
+ && witness.targetBindingDigest === receipt.targetBindingDigest
2207
+ && witness.baselineSnapshotDigest === baselineSnapshotDigest
2208
+ && witness.postSendDeltaDigest === receipt.postSendDeltaDigest
2209
+ && witness.operationUserEvidenceDigest === receipt.userTurnEvidenceDigest
2210
+ && (witness.userTurnId === undefined || witness.userTurnId === receipt.userTurnId);
2211
+ }
2212
+ function submissionWitnessMatchesEstablishment(witness, establishment, baselineSnapshotDigest) {
2213
+ return witness.actionId === establishment.causalSendActionId
2214
+ && witness.actionKind === "send"
2215
+ && witness.targetBindingDigest === establishment.targetBindingDigest
2216
+ && witness.baselineSnapshotDigest === baselineSnapshotDigest
2217
+ && witness.postSendDeltaDigest === establishment.postSendDeltaDigest
2218
+ && witness.operationUserEvidenceDigest === establishment.userTurnEvidenceDigest
2219
+ && (witness.userTurnId === undefined || witness.userTurnId === establishment.userTurnId);
2220
+ }
2221
+ function actionMatchesIntent(action, request) {
2222
+ return action.actionId === request.actionId
2223
+ && action.kind === request.kind
2224
+ && action.repeatPolicy === request.repeatPolicy
2225
+ && action.requestDigest === request.requestDigest
2226
+ && action.targetDigest === request.targetBindingDigest;
2227
+ }
2228
+ function conflictingActionForIntent(state, request) {
2229
+ if (request.kind === "file_handoff" || request.kind === "send") {
2230
+ return Object.values(state.actions).find(action => action.kind === request.kind);
2231
+ }
2232
+ if (request.kind === "configuration_set"
2233
+ || request.kind === "tool_set"
2234
+ || request.kind === "composer_set"
2235
+ || request.kind === "power_select") {
2236
+ return Object.values(state.actions).find(action => action.kind === request.kind && action.outcome === undefined);
2237
+ }
2238
+ return undefined;
2239
+ }
2240
+ function phaseReached(current, desired) {
2241
+ if (current === desired)
2242
+ return true;
2243
+ const order = ["prepared", "handoff_pending", "ready", "send_pending", "submitted", "generating", "capturing", "completed"];
2244
+ if (desired === "uncertain")
2245
+ return current === "uncertain" || current === "completed";
2246
+ if (current === "uncertain")
2247
+ return false;
2248
+ const currentIndex = order.indexOf(current);
2249
+ const desiredIndex = order.indexOf(desired);
2250
+ return currentIndex >= 0 && desiredIndex >= 0 && currentIndex > desiredIndex;
2251
+ }
2252
+ function isRevisionConflict(error) {
2253
+ return error instanceof OperationJournalError && error.code === "revision_conflict";
2254
+ }
2255
+ function validateTargetResolution(value) {
2256
+ if (!value || typeof value !== "object" || value.target === undefined) {
2257
+ throw new OperationServiceError("invalid_target_resolution", "Target resolution is incomplete.");
2258
+ }
2259
+ if (value.configurationReceiptDigest !== undefined)
2260
+ assertDigest(value.configurationReceiptDigest, "configurationReceiptDigest");
2261
+ if (value.composerReceiptDigest !== undefined)
2262
+ assertDigest(value.composerReceiptDigest, "composerReceiptDigest");
2263
+ }
2264
+ function validateSubmissionWitnessInput(value) {
2265
+ if (value === null
2266
+ || typeof value !== "object"
2267
+ || Array.isArray(value)
2268
+ || value.schemaVersion !== OPERATION_SUBMISSION_WITNESS_SCHEMA_VERSION
2269
+ || !UUID_PATTERN.test(value.actionId)
2270
+ || (value.actionKind !== "send" && value.actionKind !== "work_steer")
2271
+ || !DIGEST_PATTERN.test(value.targetBindingDigest)
2272
+ || !DIGEST_PATTERN.test(value.baselineSnapshotDigest)
2273
+ || !DIGEST_PATTERN.test(value.postSendDeltaDigest)
2274
+ || !DIGEST_PATTERN.test(value.operationUserEvidenceDigest)
2275
+ || (value.userTurnId !== undefined && !isSafeOpaqueId(value.userTurnId))
2276
+ || !isCanonicalTimestamp(value.observedAt)) {
2277
+ throw new OperationServiceError("invalid_submission_witness", "Submission witness evidence is invalid.");
2278
+ }
2279
+ }
2280
+ function isSafeOpaqueId(value) {
2281
+ return typeof value === "string"
2282
+ && value.length > 0
2283
+ && value.length <= 512
2284
+ && !/[\u0000-\u001f\u007f]/u.test(value);
2285
+ }
2286
+ function validateTargetEstablishmentRequest(value) {
2287
+ const normalized = readPlainServiceDataRecord(value);
2288
+ if (normalized === undefined) {
2289
+ throw new OperationServiceError("invalid_target_establishment", "Target establishment request is invalid.");
2290
+ }
2291
+ const allowedKeys = new Set([
2292
+ "operationId",
2293
+ "requestDigest",
2294
+ "targetBindingDigest",
2295
+ "anchorDigest",
2296
+ "causalSendActionId",
2297
+ "conversationId",
2298
+ "canonicalThreadUrl",
2299
+ "userTurnId",
2300
+ "userTurnEvidenceDigest",
2301
+ "postSendDeltaDigest",
2302
+ "evidenceDigest",
2303
+ "observedAt"
2304
+ ]);
2305
+ for (const key of Object.keys(normalized)) {
2306
+ if (!allowedKeys.has(key)) {
2307
+ throw new OperationServiceError("invalid_target_establishment", "Target establishment request contains unsupported fields.");
2308
+ }
2309
+ }
2310
+ for (const [label, candidate] of [
2311
+ ["operationId", normalized.operationId],
2312
+ ["causalSendActionId", normalized.causalSendActionId],
2313
+ ["conversationId", normalized.conversationId],
2314
+ ["userTurnId", normalized.userTurnId]
2315
+ ]) {
2316
+ if (typeof candidate !== "string" || candidate.trim().length === 0 || candidate.length > 512 || /[\u0000-\u001f\u007f]/u.test(candidate)) {
2317
+ throw new OperationServiceError("invalid_target_establishment", `${label} is invalid.`);
2318
+ }
2319
+ }
2320
+ if (!UUID_PATTERN.test(normalized.operationId) || !UUID_PATTERN.test(normalized.causalSendActionId)) {
2321
+ throw new OperationServiceError("invalid_target_establishment", "Target establishment operation/action identity is invalid.");
2322
+ }
2323
+ for (const [label, digest] of [
2324
+ ["requestDigest", normalized.requestDigest],
2325
+ ["targetBindingDigest", normalized.targetBindingDigest],
2326
+ ["anchorDigest", normalized.anchorDigest],
2327
+ ["userTurnEvidenceDigest", normalized.userTurnEvidenceDigest],
2328
+ ["postSendDeltaDigest", normalized.postSendDeltaDigest],
2329
+ ["evidenceDigest", normalized.evidenceDigest]
2330
+ ]) {
2331
+ if (typeof digest !== "string" || !DIGEST_PATTERN.test(digest)) {
2332
+ throw new OperationServiceError("invalid_target_establishment", `${label} is invalid.`);
2333
+ }
2334
+ }
2335
+ if (typeof normalized.canonicalThreadUrl !== "string" || normalized.canonicalThreadUrl.length === 0 || normalized.canonicalThreadUrl.length > 4096) {
2336
+ throw new OperationServiceError("invalid_target_establishment", "canonicalThreadUrl is invalid.");
2337
+ }
2338
+ let parsed;
2339
+ try {
2340
+ parsed = new URL(normalized.canonicalThreadUrl);
2341
+ }
2342
+ catch {
2343
+ throw new OperationServiceError("invalid_target_establishment", "canonicalThreadUrl is invalid.");
2344
+ }
2345
+ if (parsed.protocol !== "https:"
2346
+ || parsed.username !== ""
2347
+ || parsed.password !== ""
2348
+ || parsed.search !== ""
2349
+ || parsed.hash !== ""
2350
+ || parsed.toString() !== normalized.canonicalThreadUrl) {
2351
+ throw new OperationServiceError("invalid_target_establishment", "canonicalThreadUrl must be canonical HTTPS without credentials, query, or fragment.");
2352
+ }
2353
+ if (normalized.observedAt !== undefined && (typeof normalized.observedAt !== "string" || !isCanonicalTimestamp(normalized.observedAt))) {
2354
+ throw new OperationServiceError("invalid_target_establishment", "observedAt must be a canonical UTC timestamp.");
2355
+ }
2356
+ return Object.freeze(normalized);
2357
+ }
2358
+ function isCanonicalTimestamp(value) {
2359
+ const parsed = Date.parse(value);
2360
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
2361
+ && Number.isFinite(parsed)
2362
+ && new Date(parsed).toISOString() === value;
2363
+ }
2364
+ function readPlainServiceDataRecord(value) {
2365
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2366
+ return undefined;
2367
+ try {
2368
+ const prototype = Object.getPrototypeOf(value);
2369
+ if (prototype !== Object.prototype && prototype !== null)
2370
+ return undefined;
2371
+ const descriptors = Object.getOwnPropertyDescriptors(value);
2372
+ const normalized = Object.create(null);
2373
+ for (const key of Reflect.ownKeys(descriptors)) {
2374
+ if (typeof key !== "string")
2375
+ return undefined;
2376
+ const descriptor = descriptors[key];
2377
+ if (descriptor === undefined
2378
+ || descriptor.get !== undefined
2379
+ || descriptor.set !== undefined
2380
+ || !("value" in descriptor))
2381
+ return undefined;
2382
+ normalized[key] = descriptor.value;
2383
+ }
2384
+ return normalized;
2385
+ }
2386
+ catch {
2387
+ return undefined;
2388
+ }
2389
+ }
2390
+ function assertDigest(value, label) {
2391
+ if (!DIGEST_PATTERN.test(value))
2392
+ throw new OperationServiceError("invalid_digest", `${label} must be a canonical HMAC digest.`);
2393
+ }
2394
+ function isAbortSignal(value) {
2395
+ return value !== null && typeof value === "object" && typeof value.aborted === "boolean" && typeof value.addEventListener === "function";
2396
+ }
2397
+ function submissionFromCompleted(state) {
2398
+ const receipt = state.receipt;
2399
+ if (receipt === undefined)
2400
+ throw new OperationServiceError("operation_state_corrupt", "Completed operation has no receipt.");
2401
+ const action = originalSendAction(state);
2402
+ return {
2403
+ operationId: state.operationId,
2404
+ requestDigest: state.requestDigest,
2405
+ surface: state.surface,
2406
+ targetBindingDigest: receipt.targetBindingDigest,
2407
+ kind: "completed_receipt",
2408
+ ...(action === undefined ? {} : { actionId: action.actionId }),
2409
+ evidenceDigest: receipt.ownershipEvidenceDigest,
2410
+ userTurnId: receipt.userTurnId,
2411
+ userTurnEvidenceDigest: receipt.userTurnEvidenceDigest,
2412
+ assistantTurnId: receipt.assistantTurnId
2413
+ };
2414
+ }
2415
+ const TARGET_RESOLUTION_BLOCKER_CODES = new Set([
2416
+ "operation_cancelled",
2417
+ "operation_timeout",
2418
+ "operation_state_corrupt",
2419
+ "target_binding_mismatch",
2420
+ "target_evidence_unavailable",
2421
+ "configuration_drift",
2422
+ "composer_drift",
2423
+ "tab_ownership_conflict",
2424
+ "runtime_incompatible",
2425
+ "backend_unavailable",
2426
+ "browser_bridge_unavailable",
2427
+ "login_required",
2428
+ "captcha",
2429
+ "rate_limited",
2430
+ "permission_required",
2431
+ "needs_confirmation",
2432
+ "selector_drift",
2433
+ "journal_unavailable",
2434
+ "port_protocol_violation"
2435
+ ]);
2436
+ const NON_RECOVERABLE_TARGET_BLOCKERS = new Set([
2437
+ "operation_state_corrupt",
2438
+ "target_binding_mismatch",
2439
+ "runtime_incompatible",
2440
+ "port_protocol_violation"
2441
+ ]);
2442
+ /**
2443
+ * Convert a read-only target-resolution failure into a redacted operation
2444
+ * result without manufacturing a target binding. Error messages are never
2445
+ * copied: provider errors may contain URLs, account details, or request data.
2446
+ */
2447
+ function submissionFromTargetResolutionFailure(state, handle, error, signal) {
2448
+ const code = targetResolutionBlockerCode(error, signal);
2449
+ const observationRequired = state.mutationBoundary !== "none"
2450
+ || !NON_RECOVERABLE_TARGET_BLOCKERS.has(code);
2451
+ const blocker = {
2452
+ code,
2453
+ observationRequired,
2454
+ mutationBoundary: state.mutationBoundary
2455
+ };
2456
+ const identity = {
2457
+ operationId: state.operationId,
2458
+ requestDigest: state.requestDigest,
2459
+ surface: state.surface,
2460
+ ...(handle.targetBindingDigest === undefined
2461
+ ? {}
2462
+ : { targetBindingDigest: handle.targetBindingDigest })
2463
+ };
2464
+ if (code === "operation_cancelled" || code === "operation_timeout") {
2465
+ return {
2466
+ ...identity,
2467
+ kind: "cancelled",
2468
+ blocker: { ...blocker, code }
2469
+ };
2470
+ }
2471
+ if (state.phase === "uncertain" || state.mutationBoundary !== "none") {
2472
+ return { ...identity, kind: "uncertain", blocker };
2473
+ }
2474
+ return { ...identity, kind: "blocked", blocker };
2475
+ }
2476
+ function targetResolutionBlockerCode(error, signal) {
2477
+ if (signal.aborted)
2478
+ return "operation_cancelled";
2479
+ const code = safeOwnErrorCode(error);
2480
+ if (code !== undefined && TARGET_RESOLUTION_BLOCKER_CODES.has(code)) {
2481
+ return code;
2482
+ }
2483
+ switch (code) {
2484
+ case "rate_limit":
2485
+ return "rate_limited";
2486
+ case "page_affinity_mismatch":
2487
+ return "target_binding_mismatch";
2488
+ case "adapter_incomplete":
2489
+ case "capture_incomplete":
2490
+ case "unsupported_browser_primitive":
2491
+ case "not_initialized":
2492
+ case "invalid_target_resolution":
2493
+ case "invalid_digest":
2494
+ return "port_protocol_violation";
2495
+ case "adapter_unavailable":
2496
+ return "backend_unavailable";
2497
+ case "capture_failed":
2498
+ default:
2499
+ return "target_evidence_unavailable";
2500
+ }
2501
+ }
2502
+ /** Read only an own data property so a hostile thrown accessor is never invoked. */
2503
+ function safeOwnErrorCode(error) {
2504
+ if (error === null || (typeof error !== "object" && typeof error !== "function"))
2505
+ return undefined;
2506
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
2507
+ return descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string"
2508
+ ? descriptor.value
2509
+ : undefined;
2510
+ }
2511
+ function completedFromState(handle, state) {
2512
+ const receipt = state.receipt;
2513
+ if (receipt === undefined)
2514
+ throw new OperationServiceError("operation_state_corrupt", "Completed operation has no receipt.");
2515
+ return {
2516
+ kind: "completed",
2517
+ operationId: state.operationId,
2518
+ requestDigest: state.requestDigest,
2519
+ targetBindingDigest: receipt.targetBindingDigest,
2520
+ attempts: 0,
2521
+ turn: {
2522
+ userTurnId: receipt.userTurnId,
2523
+ assistantTurnId: receipt.assistantTurnId,
2524
+ userTurnEvidenceDigest: receipt.userTurnEvidenceDigest,
2525
+ ownershipEvidenceDigest: receipt.ownershipEvidenceDigest
2526
+ },
2527
+ response: {
2528
+ contentAvailable: receipt.contentAvailable,
2529
+ rawContentAvailable: false,
2530
+ ...(receipt.responseDigest === undefined ? {} : {
2531
+ text: {
2532
+ digest: receipt.responseDigest,
2533
+ ...(receipt.responseBytes === undefined ? {} : { bytes: receipt.responseBytes })
2534
+ }
2535
+ }),
2536
+ artifacts: receipt.artifacts.map(artifact => ({
2537
+ kind: artifact.kind,
2538
+ ordinal: artifact.ordinal,
2539
+ sourceIdentityDigest: artifact.sourceIdentityDigest,
2540
+ ...(artifact.sha256 === undefined ? {} : { contentDigest: `sha256:${artifact.sha256}` }),
2541
+ ...(artifact.bytes === undefined ? {} : { bytes: artifact.bytes }),
2542
+ ...(artifact.mimeType === undefined ? {} : { mimeType: artifact.mimeType })
2543
+ })),
2544
+ finishReason: receipt.finishReason
2545
+ }
2546
+ };
2547
+ }
2548
+ function validateReceiptIdentity(receipt, operationId, requestDigest, targetBindingDigest) {
2549
+ if (receipt.schemaVersion !== OPERATION_RECEIPT_SCHEMA_VERSION
2550
+ || receipt.operationId !== operationId
2551
+ || receipt.requestDigest !== requestDigest
2552
+ || receipt.targetBindingDigest !== targetBindingDigest) {
2553
+ throw new OperationServiceError("operation_receipt_mismatch", "Terminal receipt identity does not match the durable operation.");
2554
+ }
2555
+ }
2556
+ function sameReceipt(left, right) {
2557
+ const { completedAt: _left, ...leftWithoutTime } = left;
2558
+ const { completedAt: _right, ...rightWithoutTime } = right;
2559
+ return canonicalJson(leftWithoutTime) === canonicalJson(rightWithoutTime);
2560
+ }
2561
+ /**
2562
+ * A second collector may arrive after the first has enriched the terminal
2563
+ * receipt with transfer facts. It must converge to that durable receipt when
2564
+ * the owned turn and artifact identities are exact, while never accepting a
2565
+ * reordered or substituted artifact list.
2566
+ */
2567
+ function sameTerminalReceiptIdentity(left, right) {
2568
+ const withoutArtifacts = (receipt) => {
2569
+ const { artifacts: _artifacts, completedAt: _completedAt, ...identity } = receipt;
2570
+ return identity;
2571
+ };
2572
+ if (canonicalJson(withoutArtifacts(left)) !== canonicalJson(withoutArtifacts(right)))
2573
+ return false;
2574
+ if (left.artifacts.length !== right.artifacts.length)
2575
+ return false;
2576
+ return left.artifacts.every((artifact, index) => {
2577
+ const expected = right.artifacts[index];
2578
+ return expected !== undefined
2579
+ && artifact.schemaVersion === expected.schemaVersion
2580
+ && artifact.operationId === expected.operationId
2581
+ && artifact.artifactKey === expected.artifactKey
2582
+ && artifact.assistantTurnId === expected.assistantTurnId
2583
+ && artifact.sourceIdentityDigest === expected.sourceIdentityDigest
2584
+ && artifact.kind === expected.kind
2585
+ && artifact.ordinal === expected.ordinal
2586
+ && artifact.mimeType === expected.mimeType
2587
+ && (expected.bytes === undefined || artifact.bytes === expected.bytes)
2588
+ && (expected.sha256 === undefined || artifact.sha256 === expected.sha256);
2589
+ });
2590
+ }
2591
+ function controlReceiptFromAction(request, action, observedAt) {
2592
+ const receipt = {
2593
+ schemaVersion: "chatgpt.browser_control.operation_control_receipt.v1",
2594
+ controlActionId: request.controlActionId,
2595
+ parentOperationId: request.operationId,
2596
+ parentRequestDigest: request.parentRequestDigest,
2597
+ parentTargetBindingDigest: request.parentTargetBindingDigest,
2598
+ expectedAssistantTurnId: request.expectedAssistantTurnId,
2599
+ requestDigest: request.requestDigest,
2600
+ action: request.action,
2601
+ outcome: action.outcome,
2602
+ observedAt
2603
+ };
2604
+ if (action.evidenceDigest !== undefined)
2605
+ receipt.evidenceDigest = action.evidenceDigest;
2606
+ if (action.blockerCode !== undefined)
2607
+ receipt.blockerCode = action.blockerCode;
2608
+ return receipt;
2609
+ }
2610
+ function validateSteerVerificationReceipt(value, prepared, genericReceipt) {
2611
+ const record = readPlainServiceDataRecord(value);
2612
+ if (record === undefined) {
2613
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer verification receipt is not a plain data record.");
2614
+ }
2615
+ const required = [
2616
+ "schemaVersion",
2617
+ "baselineSnapshotDigest",
2618
+ "preparedDigest",
2619
+ "assistantTurnId",
2620
+ "assistantBranchId",
2621
+ "assistantParentTurnId",
2622
+ "userTurnId",
2623
+ "userTurnEvidenceDigest",
2624
+ "postSendDeltaDigest",
2625
+ "evidenceDigest"
2626
+ ];
2627
+ const keys = Object.keys(record).sort();
2628
+ if (keys.length !== required.length || keys.some((key, index) => key !== required.slice().sort()[index])) {
2629
+ throw new OperationServiceError("operation_state_corrupt", "Work-steer verification receipt contains unsupported fields.");
2630
+ }
2631
+ if (record.schemaVersion !== CONTROL_COORDINATOR_SCHEMA_VERSION
2632
+ || record.baselineSnapshotDigest !== prepared.baselineSnapshotDigest
2633
+ || record.preparedDigest !== prepared.preparedDigest
2634
+ || record.assistantTurnId !== prepared.expectedAssistantTurnId
2635
+ || record.assistantBranchId !== prepared.assistantBranchId
2636
+ || record.assistantParentTurnId !== prepared.assistantParentTurnId
2637
+ || record.evidenceDigest !== genericReceipt.evidenceDigest
2638
+ || typeof record.userTurnId !== "string"
2639
+ || !isSafeOpaqueId(record.userTurnId)
2640
+ || typeof record.baselineSnapshotDigest !== "string"
2641
+ || typeof record.preparedDigest !== "string"
2642
+ || typeof record.userTurnEvidenceDigest !== "string"
2643
+ || typeof record.postSendDeltaDigest !== "string"
2644
+ || typeof record.evidenceDigest !== "string"
2645
+ || !DIGEST_PATTERN.test(record.baselineSnapshotDigest)
2646
+ || !DIGEST_PATTERN.test(record.preparedDigest)
2647
+ || !DIGEST_PATTERN.test(record.userTurnEvidenceDigest)
2648
+ || !DIGEST_PATTERN.test(record.postSendDeltaDigest)
2649
+ || !DIGEST_PATTERN.test(record.evidenceDigest)) {
2650
+ throw new OperationServiceError("operation_request_mismatch", "Work-steer verification receipt is not bound to the durable prepared action.");
2651
+ }
2652
+ }
2653
+ function stagingKinds(request) {
2654
+ const kinds = [];
2655
+ const configuration = request.configuration;
2656
+ if (configuration !== undefined) {
2657
+ if (configuration.experience !== undefined
2658
+ || configuration.model !== undefined
2659
+ || configuration.modelVersion !== undefined
2660
+ || configuration.mode !== undefined
2661
+ || configuration.additional !== undefined) {
2662
+ kinds.push("configuration_set");
2663
+ }
2664
+ if (configuration.tools !== undefined)
2665
+ kinds.push("tool_set");
2666
+ if (configuration.reasoning !== undefined)
2667
+ kinds.push("power_select");
2668
+ }
2669
+ kinds.push("composer_set");
2670
+ return kinds;
2671
+ }
2672
+ function stagingReceiptFromAction(identity, action) {
2673
+ if (action.outcome === undefined || action.receiptAt === undefined) {
2674
+ throw new OperationServiceError("operation_state_corrupt", "Settled staging action is incomplete.");
2675
+ }
2676
+ if (action.outcome !== "uncertain" && action.evidenceDigest === undefined) {
2677
+ throw new OperationServiceError("operation_state_corrupt", "Settled staging action is missing current-state evidence.");
2678
+ }
2679
+ return {
2680
+ schemaVersion: "chatgpt.browser_control.operation_staging_receipt.v1",
2681
+ operationId: identity.operationId,
2682
+ requestDigest: identity.requestDigest,
2683
+ targetBindingDigest: identity.targetBindingDigest,
2684
+ actionId: identity.actionId,
2685
+ kind: identity.kind,
2686
+ desiredStateDigest: identity.desiredStateDigest,
2687
+ outcome: action.outcome,
2688
+ mutation: "attempted",
2689
+ ...(action.evidenceDigest === undefined ? {} : { currentStateDigest: action.evidenceDigest }),
2690
+ ...(action.evidenceDigest === undefined ? {} : { evidenceDigest: action.evidenceDigest }),
2691
+ ...(action.blockerCode === undefined ? {} : { blockerCode: action.blockerCode }),
2692
+ observedAt: action.receiptAt
2693
+ };
2694
+ }
2695
+ function stagingBlockerCode(kind, code) {
2696
+ if (code === "operation_cancelled" || code === "operation_timeout" || code === "journal_unavailable" || code === "target_binding_mismatch" || code === "target_evidence_unavailable" || code === "port_protocol_violation") {
2697
+ return code;
2698
+ }
2699
+ if (kind === "composer_set")
2700
+ return "composer_drift";
2701
+ return "configuration_drift";
2702
+ }
2703
+ function findArtifactTransfer(state, assistantTurnId, artifact) {
2704
+ const matches = Object.values(state.artifactTransfers ?? {}).filter(transfer => {
2705
+ const intent = transfer.intent;
2706
+ return intent !== undefined
2707
+ && intent.operationId === artifact.operationId
2708
+ && intent.assistantTurnId === assistantTurnId
2709
+ && intent.sourceIdentityDigest === artifact.sourceIdentityDigest
2710
+ && intent.kind === artifact.kind
2711
+ && intent.ordinal === artifact.ordinal;
2712
+ });
2713
+ if (matches.length === 0)
2714
+ return undefined;
2715
+ // A completed receipt wins over a stale intent if a legacy/crash prefix
2716
+ // somehow contains both identities. More than one settled receipt for the
2717
+ // same terminal artifact is not safely projectable.
2718
+ const receipts = matches.filter(transfer => transfer.receipt !== undefined);
2719
+ if (receipts.length > 1) {
2720
+ const first = receipts[0].receipt;
2721
+ if (receipts.some(candidate => canonicalJson(candidate.receipt) !== canonicalJson(first))) {
2722
+ throw new OperationServiceError("operation_state_corrupt", "Multiple artifact transfer receipts conflict for one terminal artifact.");
2723
+ }
2724
+ }
2725
+ return receipts[0] ?? matches[0];
2726
+ }
2727
+ function artifactTransferIdentityMatches(intent, lookup) {
2728
+ return intent.operationId === lookup.operationId
2729
+ && intent.requestDigest === lookup.requestDigest
2730
+ && intent.targetBindingDigest === lookup.targetBindingDigest
2731
+ && intent.assistantTurnId === lookup.assistantTurnId
2732
+ && intent.sourceIdentityDigest === lookup.sourceIdentityDigest
2733
+ && intent.kind === lookup.kind
2734
+ && intent.ordinal === lookup.ordinal
2735
+ && intent.transferActionId === lookup.transferActionId
2736
+ && intent.destinationIdentityDigest === lookup.destinationIdentityDigest;
2737
+ }
2738
+ function sameArtifactTransferIdentity(left, right) {
2739
+ return left.operationId === right.operationId
2740
+ && left.requestDigest === right.requestDigest
2741
+ && left.targetBindingDigest === right.targetBindingDigest
2742
+ && left.assistantTurnId === right.assistantTurnId
2743
+ && left.sourceIdentityDigest === right.sourceIdentityDigest
2744
+ && left.kind === right.kind
2745
+ && left.ordinal === right.ordinal
2746
+ && left.transferActionId === right.transferActionId;
2747
+ }
2748
+ function artifactTransferFlightKey(identity) {
2749
+ return [
2750
+ identity.operationId,
2751
+ identity.requestDigest,
2752
+ identity.assistantTurnId,
2753
+ identity.sourceIdentityDigest,
2754
+ identity.kind,
2755
+ String(identity.ordinal)
2756
+ ].join("\0");
2757
+ }
2758
+ function projectArtifactTransferReceipt(observed, transfer) {
2759
+ if (transfer.operationId !== observed.operationId
2760
+ || transfer.assistantTurnId !== observed.assistantTurnId
2761
+ || transfer.sourceIdentityDigest !== observed.sourceIdentityDigest
2762
+ || transfer.kind !== observed.kind
2763
+ || transfer.ordinal !== observed.ordinal) {
2764
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer receipt identity does not match the terminal artifact.");
2765
+ }
2766
+ if (observed.bytes !== undefined && transfer.bytes !== undefined && observed.bytes !== transfer.bytes) {
2767
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer changed an observed artifact byte count.");
2768
+ }
2769
+ if (observed.sha256 !== undefined && transfer.sha256 !== undefined && observed.sha256 !== transfer.sha256) {
2770
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer changed an observed artifact digest.");
2771
+ }
2772
+ return {
2773
+ ...observed,
2774
+ ...(transfer.outputKey === undefined ? {} : { outputKey: transfer.outputKey }),
2775
+ ...(transfer.bytes === undefined ? {} : { bytes: transfer.bytes }),
2776
+ ...(transfer.sha256 === undefined ? {} : { sha256: transfer.sha256 }),
2777
+ status: transfer.status,
2778
+ ...(transfer.blockerCode === undefined ? {} : { blockerCode: transfer.blockerCode })
2779
+ };
2780
+ }
2781
+ function assertTerminalArtifactProjection(projected, observed) {
2782
+ if (projected.artifacts.length !== observed.artifacts.length) {
2783
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer changed the terminal artifact count.");
2784
+ }
2785
+ for (let index = 0; index < observed.artifacts.length; index += 1) {
2786
+ const expected = observed.artifacts[index];
2787
+ const actual = projected.artifacts[index];
2788
+ if (expected === undefined || actual === undefined) {
2789
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer changed terminal artifact ordering.");
2790
+ }
2791
+ if (expected.artifactKey !== actual.artifactKey
2792
+ || expected.operationId !== actual.operationId
2793
+ || expected.assistantTurnId !== actual.assistantTurnId
2794
+ || expected.sourceIdentityDigest !== actual.sourceIdentityDigest
2795
+ || expected.kind !== actual.kind
2796
+ || expected.ordinal !== actual.ordinal
2797
+ || expected.mimeType !== actual.mimeType) {
2798
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Artifact transfer changed terminal artifact identity.");
2799
+ }
2800
+ if (actual.status === "available") {
2801
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Transfer capture completed with an unsettled artifact.");
2802
+ }
2803
+ if ((actual.status === "partial" || actual.status === "blocked") && actual.blockerCode === undefined) {
2804
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Incomplete artifact transfer is missing a blocker.");
2805
+ }
2806
+ if (actual.status === "transferred" && (actual.outputKey === undefined || actual.bytes === undefined || actual.sha256 === undefined || actual.blockerCode !== undefined)) {
2807
+ throw new OperationServiceError("artifact_transfer_protocol_violation", "Transferred artifact receipt is incomplete.");
2808
+ }
2809
+ }
2810
+ }