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

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