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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. package/references/streaming.md +6 -0
@@ -0,0 +1,2023 @@
1
+ import { getProcessTabCoordinator } from "../runtime/tab-coordinator.js";
2
+ import { OperationRuntimeContext } from "../runtime/operation-context.js";
3
+ import { observeBrowserPage } from "./browser-observation.js";
4
+ import { bindBrowserTarget } from "./browser-target.js";
5
+ import { canonicalJson } from "./canonical.js";
6
+ import { executePreparedSendOnce, prepareSendOnce, recoverSendOnce, runSendOnce, verifyPreparedSendOnce } from "./send-once.js";
7
+ import { revalidateOperationFile } from "./file-identity.js";
8
+ import { CONTROL_COORDINATOR_SCHEMA_VERSION, CONTROL_POSTCONDITION_RETRY_POLICY } from "./control.js";
9
+ import { transferOperationArtifact } from "./artifact-transfer.js";
10
+ export class OperationBrowserAdapterError extends Error {
11
+ code;
12
+ constructor(code) {
13
+ super(code === "input_file_changed"
14
+ ? "An operation input file changed before the file handoff; no browser handoff was attempted."
15
+ : "The operation browser adapter could not prove the requested browser action safely.");
16
+ this.name = "OperationBrowserAdapterError";
17
+ this.code = code;
18
+ }
19
+ }
20
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
21
+ const OPAQUE_ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
22
+ const OPAQUE_THREAD_URL_PATTERN = /^https:\/\/opaque\.invalid\/thread\/[0-9a-f]{64}$/u;
23
+ const CLAIM_EVIDENCE_DIGEST_DOMAIN = "codex-chatgpt-control/tab-claim-evidence/v1";
24
+ const MAX_DEADLINE_AT = Date.UTC(2100, 0, 1);
25
+ const DEFAULT_TRANSACTION_TIMEOUT_MS = 30_000;
26
+ const MAX_TRANSACTION_TIMEOUT_MS = 120_000;
27
+ function isAbsolutePath(value) {
28
+ return value.startsWith("/")
29
+ || /^[A-Za-z]:[\\/]/u.test(value)
30
+ || value.startsWith("\\\\");
31
+ }
32
+ /**
33
+ * Build the operation browser adapter over one captured page.
34
+ *
35
+ * The adapter is deliberately a composition layer: journal persistence stays
36
+ * in OperationService, browser observations are supplied by narrow injected
37
+ * primitives (or the conservative browser-observation default), and every
38
+ * non-repeatable call is made at one explicit site. No method reads
39
+ * RuntimeEnv.page and no method retries a browser mutation.
40
+ */
41
+ export function createOperationBrowserAdapter(options) {
42
+ options = normalizeAdapterOptions(options);
43
+ const page = options.page;
44
+ const runtimeCapture = options.runtimeContext?.capture();
45
+ if (runtimeCapture !== undefined && runtimeCapture.page !== page) {
46
+ throw new OperationBrowserAdapterError("page_affinity_mismatch");
47
+ }
48
+ const coordinator = options.coordinator ?? getProcessTabCoordinator();
49
+ const transactionTimeoutMs = options.transactionTimeoutMs ?? DEFAULT_TRANSACTION_TIMEOUT_MS;
50
+ const bindings = new Map();
51
+ const collectorContexts = new Map();
52
+ let recoveryPromise;
53
+ const ensureRecovered = async (operationId, requestDigest, surface, signal) => {
54
+ const recovery = options.recovery;
55
+ if (recovery === undefined) {
56
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
57
+ }
58
+ assertNativeAbortSignal(signal);
59
+ if (operationId !== recovery.operationId
60
+ || requestDigest !== recovery.requestDigest
61
+ || surface !== recovery.surface) {
62
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
63
+ }
64
+ recoveryPromise ??= hydrateRecoveredTarget(options, recovery, page, coordinator);
65
+ const binding = await recoveryPromise;
66
+ bindings.set(recovery.operationId, binding);
67
+ return binding;
68
+ };
69
+ const resolveTarget = async (request) => {
70
+ if (options.recovery !== undefined) {
71
+ try {
72
+ const binding = await ensureRecovered(request.operationId, request.requestDigest, request.surface, request.signal);
73
+ return Object.freeze({ target: binding.target });
74
+ }
75
+ catch (error) {
76
+ if (error instanceof OperationBrowserAdapterError)
77
+ throw error;
78
+ throw new OperationBrowserAdapterError(errorCode(error));
79
+ }
80
+ }
81
+ let probe;
82
+ try {
83
+ probe = await resolveProbe(options, request);
84
+ assertProbePage(page, probe.page);
85
+ if (request.target.type === "new") {
86
+ if (probe.targetLifecycle !== undefined && probe.targetLifecycle !== "new_pending") {
87
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
88
+ }
89
+ }
90
+ else if (probe.targetLifecycle === "new_pending") {
91
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
92
+ }
93
+ assertStaticTarget(request.target, probe.evidence, options.resolveTargetEvidence !== undefined);
94
+ if (runtimeCapture !== undefined) {
95
+ assertRuntimeAffinity(runtimeCapture, page, probe);
96
+ }
97
+ const owner = Object.freeze({
98
+ ...options.owner,
99
+ operationId: request.operationId
100
+ });
101
+ const input = {
102
+ page,
103
+ evidence: probe.evidence,
104
+ ...(request.target.type === "new"
105
+ ? { targetLifecycle: "new_pending" }
106
+ : probe.targetLifecycle === undefined ? {} : { targetLifecycle: probe.targetLifecycle }),
107
+ ...((probe.newTargetAnchorDigest ?? options.newTargetAnchorDigest) === undefined
108
+ ? {}
109
+ : { newTargetAnchorDigest: probe.newTargetAnchorDigest ?? options.newTargetAnchorDigest }),
110
+ ...((probe.blankTaskEvidenceDigest ?? options.blankTaskEvidenceDigest) === undefined
111
+ ? {}
112
+ : { blankTaskEvidenceDigest: probe.blankTaskEvidenceDigest ?? options.blankTaskEvidenceDigest }),
113
+ ...(probe.authoritativeClaim === undefined ? {} : { authoritativeClaim: probe.authoritativeClaim }),
114
+ capabilities: {
115
+ ...(options.capabilities ?? {}),
116
+ ...(probe.capabilities ?? {})
117
+ },
118
+ evidenceDigest: options.evidenceDigest,
119
+ owner,
120
+ coordinator
121
+ };
122
+ const binding = bindBrowserTarget(input);
123
+ const previous = bindings.get(request.operationId);
124
+ if (previous !== undefined && canonicalJson(previous.target) !== canonicalJson(binding.target)) {
125
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
126
+ }
127
+ bindings.set(request.operationId, binding);
128
+ return Object.freeze({ target: binding.target });
129
+ }
130
+ catch (error) {
131
+ if (error instanceof OperationBrowserAdapterError)
132
+ throw error;
133
+ throw new OperationBrowserAdapterError(errorCode(error));
134
+ }
135
+ };
136
+ const observeStaging = async (request) => {
137
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
138
+ if (binding === undefined)
139
+ return unavailableStage("target");
140
+ try {
141
+ return await runReadTransaction(binding, request.operationId, async (context) => {
142
+ if (options.submission?.observeStaging === undefined)
143
+ return unavailableStage("unknown");
144
+ return await options.submission.observeStaging(request, context.page, context.target);
145
+ }, options.observeCurrentTarget, options.evidenceDigest, { timeoutMs: transactionTimeoutMs });
146
+ }
147
+ catch {
148
+ return unavailableStage("target");
149
+ }
150
+ };
151
+ const executeFileHandoffOnce = async (request) => {
152
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
153
+ if (binding === undefined)
154
+ return { status: "not_satisfied", blockerCode: "target_binding_mismatch" };
155
+ const files = matchFileManifest(options.files, options.fileManifestDigest, request.manifest);
156
+ if (files === undefined || options.submission?.handoffFiles === undefined) {
157
+ return { status: "not_satisfied", blockerCode: "attachment_manifest_mismatch" };
158
+ }
159
+ // Target proof gets its own short read transaction. Hashing/revalidation
160
+ // stays outside the actor, so a large file cannot monopolize the tab.
161
+ try {
162
+ await runReadTransaction(binding, request.operationId, async () => undefined, options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-file-handoff-target"));
163
+ for (const identity of files) {
164
+ if (request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt) {
165
+ // A durable file-handoff intent already exists by the time this port
166
+ // is called. Even a cancellation before the provider mutation is
167
+ // therefore observation-only; never let a caller retry the handoff.
168
+ return { status: "uncertain", quarantine: "caller" };
169
+ }
170
+ try {
171
+ await revalidateOperationFile(identity);
172
+ }
173
+ catch {
174
+ return { status: "not_satisfied", blockerCode: "input_file_changed" };
175
+ }
176
+ }
177
+ if (request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt) {
178
+ return { status: "uncertain", quarantine: "caller" };
179
+ }
180
+ // This is the sole file-handoff mutation site. There is no catch/retry
181
+ // branch that invokes handoffFiles a second time.
182
+ return await runMutationTransaction(binding, request.operationId, async (context) => {
183
+ const providerRequest = Object.freeze({
184
+ ...request,
185
+ signal: context.acquisition.signal,
186
+ ...(context.acquisition.timing.deadlineAt === undefined
187
+ ? (request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
188
+ : { deadlineAt: context.acquisition.timing.deadlineAt })
189
+ });
190
+ const result = await options.submission.handoffFiles(providerRequest, files, context.page, context.target);
191
+ // A provider may finish with a success-looking acknowledgement after
192
+ // the coordinator has already aborted its in-flight mutation. The
193
+ // acknowledgement is no longer safe to treat as exact; recovery must
194
+ // observe the live attachment surface and may not repeat the handoff.
195
+ return context.acquisition.signal.aborted
196
+ ? { status: "uncertain", quarantine: "caller" }
197
+ : result;
198
+ }, options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-file-handoff", "mutation"));
199
+ }
200
+ catch {
201
+ return request.signal?.aborted || request.deadlineAt !== undefined && Date.now() >= request.deadlineAt
202
+ ? { status: "uncertain", quarantine: "caller" }
203
+ : { status: "uncertain", quarantine: "provider" };
204
+ }
205
+ };
206
+ const observeAttachments = async (request) => {
207
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
208
+ if (binding === undefined)
209
+ return { status: "unavailable" };
210
+ try {
211
+ return await runReadTransaction(binding, request.operationId, async (context) => {
212
+ if (options.submission?.observeAttachments === undefined)
213
+ return { status: "unavailable" };
214
+ return await options.submission.observeAttachments(request, context.page, context.target);
215
+ }, options.observeCurrentTarget, options.evidenceDigest, { timeoutMs: transactionTimeoutMs });
216
+ }
217
+ catch {
218
+ return { status: "unavailable" };
219
+ }
220
+ };
221
+ /**
222
+ * Send's four phase ports deliberately share only an operation-local abort
223
+ * controller. The controller is linked to both the caller and whichever
224
+ * coordinator transaction is currently executing. This lets the provider
225
+ * see the coordinator's cancellation without making the durable service
226
+ * aware of a page, actor, or bridge object.
227
+ */
228
+ const prepareSend = async (request) => {
229
+ const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
230
+ if (binding === undefined)
231
+ return blockedPrepareSend("target_binding_mismatch");
232
+ const baseObservers = options.submission?.sendObservers;
233
+ if (baseObservers === undefined)
234
+ return blockedPrepareSend("target_evidence_unavailable");
235
+ const linked = createLinkedAbortController(request.signal);
236
+ const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
237
+ try {
238
+ const result = await prepareSendOnce({
239
+ page,
240
+ operationId: request.operationId,
241
+ requestDigest: request.requestDigest,
242
+ surface: request.surface,
243
+ actionId: request.actionId,
244
+ expected: request.expected,
245
+ observers: send.observers,
246
+ signal: linked.controller.signal,
247
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt }),
248
+ transaction: callback => runSendTransaction(binding, request.operationId, linked.controller, options.observeCurrentTarget, options.evidenceDigest, request.deadlineAt, transactionTimeoutMs, "operation-send-prepare", "read", context => send.withContext(context, callback))
249
+ });
250
+ if (result.status === "blocked")
251
+ return resultToPrepareSend(result.result);
252
+ try {
253
+ return {
254
+ status: "prepared",
255
+ prepared: submissionPreparedFromSendOnce(result.prepared, request.expected)
256
+ };
257
+ }
258
+ catch {
259
+ return blockedPrepareSend("port_protocol_violation");
260
+ }
261
+ }
262
+ catch {
263
+ return blockedPrepareSend(phaseBlocker(linked.controller.signal, request.deadlineAt));
264
+ }
265
+ finally {
266
+ linked.cleanup();
267
+ }
268
+ };
269
+ const executePreparedSend = async (request) => {
270
+ const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
271
+ if (binding === undefined)
272
+ return blockedExecuteSend("target_binding_mismatch");
273
+ const baseObservers = options.submission?.sendObservers;
274
+ if (baseObservers === undefined)
275
+ return blockedExecuteSend("target_evidence_unavailable");
276
+ let prepared;
277
+ try {
278
+ prepared = normalizeSubmissionPreparedSend(request.prepared, request.expected, {
279
+ operationId: request.operationId,
280
+ requestDigest: request.requestDigest,
281
+ surface: request.surface,
282
+ actionId: request.actionId
283
+ });
284
+ }
285
+ catch {
286
+ return blockedExecuteSend("port_protocol_violation");
287
+ }
288
+ const linked = createLinkedAbortController(request.signal);
289
+ const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
290
+ try {
291
+ const result = await executePreparedSendOnce({
292
+ page,
293
+ prepared: prepared.prepared,
294
+ observers: send.observers,
295
+ signal: linked.controller.signal,
296
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt }),
297
+ transaction: callback => runSendTransaction(binding, request.operationId, linked.controller, options.observeCurrentTarget, options.evidenceDigest, request.deadlineAt, transactionTimeoutMs, "operation-send-execute", "mutation", context => send.withContext(context, callback))
298
+ });
299
+ if (result.status === "blocked" || result.status === "uncertain")
300
+ return result;
301
+ return {
302
+ status: result.status,
303
+ activation: result.activation,
304
+ mutationMayHaveOccurred: true
305
+ };
306
+ }
307
+ catch {
308
+ return {
309
+ status: "uncertain",
310
+ result: { status: "uncertain", quarantine: "caller" }
311
+ };
312
+ }
313
+ finally {
314
+ linked.cleanup();
315
+ }
316
+ };
317
+ const verifyPreparedSend = async (request) => {
318
+ const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
319
+ if (binding === undefined)
320
+ return { status: "blocked", blockerCode: "target_binding_mismatch" };
321
+ const baseObservers = options.submission?.sendObservers;
322
+ if (baseObservers === undefined)
323
+ return { status: "uncertain", quarantine: "provider" };
324
+ let prepared;
325
+ try {
326
+ prepared = normalizeSubmissionPreparedSend(request.prepared, request.expected, {
327
+ operationId: request.operationId,
328
+ requestDigest: request.requestDigest,
329
+ surface: request.surface,
330
+ actionId: request.actionId
331
+ });
332
+ }
333
+ catch {
334
+ return { status: "uncertain", quarantine: "caller" };
335
+ }
336
+ const linked = createLinkedAbortController(request.signal);
337
+ const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
338
+ try {
339
+ const result = await verifyPreparedSendOnce({
340
+ page,
341
+ prepared: prepared.prepared,
342
+ observers: send.observers,
343
+ activation: request.activation,
344
+ mutationMayHaveOccurred: request.mutationMayHaveOccurred,
345
+ signal: linked.controller.signal,
346
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
347
+ });
348
+ markEstablished(binding, result);
349
+ return result;
350
+ }
351
+ catch {
352
+ return { status: "uncertain", quarantine: "caller" };
353
+ }
354
+ finally {
355
+ linked.cleanup();
356
+ }
357
+ };
358
+ const recoverSend = async (request) => {
359
+ let binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
360
+ if (binding === undefined && options.recovery !== undefined) {
361
+ try {
362
+ binding = await ensureRecovered(request.operationId, request.requestDigest, request.surface, request.signal ?? options.recovery.signal);
363
+ }
364
+ catch {
365
+ return { status: "blocked", blockerCode: "target_binding_mismatch" };
366
+ }
367
+ }
368
+ if (binding === undefined)
369
+ return { status: "blocked", blockerCode: "target_binding_mismatch" };
370
+ const baseObservers = options.submission?.sendObservers;
371
+ if (baseObservers === undefined)
372
+ return { status: "uncertain", quarantine: "provider" };
373
+ const linked = createLinkedAbortController(request.signal);
374
+ const send = createPhaseSendObservers(binding, request.operationId, baseObservers, linked.controller, options.observeCurrentTarget, options.evidenceDigest, transactionTimeoutMs);
375
+ try {
376
+ const result = await recoverSendOnce({
377
+ page,
378
+ operationId: request.operationId,
379
+ requestDigest: request.requestDigest,
380
+ surface: request.surface,
381
+ actionId: request.actionId,
382
+ expected: request.expected,
383
+ durableBaseline: request.durableBaseline,
384
+ observers: send.observers,
385
+ signal: linked.controller.signal,
386
+ ...(request.deadlineAt === undefined ? {} : { deadlineAt: request.deadlineAt })
387
+ });
388
+ markEstablished(binding, result);
389
+ return result;
390
+ }
391
+ catch {
392
+ return { status: "uncertain", quarantine: "caller" };
393
+ }
394
+ finally {
395
+ linked.cleanup();
396
+ }
397
+ };
398
+ const executeFinalTabTransaction = async (request) => {
399
+ const binding = bindingFor(bindings, request.operationId, request.expected.targetBindingDigest);
400
+ if (binding === undefined) {
401
+ return { status: "blocked", blockerCode: "target_binding_mismatch" };
402
+ }
403
+ const baseObservers = options.submission?.sendObservers;
404
+ if (baseObservers === undefined)
405
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
406
+ // The precondition read is part of the short activation transaction. Each
407
+ // postcondition probe, by contrast, reacquires the same tab actor for one
408
+ // bounded read and releases it before SendOnce waits for the next probe.
409
+ // The provider primitive is intentionally one-shot: it must not poll or
410
+ // sleep while this callback owns the actor.
411
+ const observers = {
412
+ observePrecondition: baseObservers.observePrecondition,
413
+ observePostcondition: async (postconditionRequest) => {
414
+ const probe = await runReadTransaction(binding, request.operationId, context => baseObservers.observePostcondition({
415
+ ...postconditionRequest,
416
+ page: context.page,
417
+ // A probe is owned by this individual short read transaction, not
418
+ // by the earlier activation transaction. Provider code must see
419
+ // the current actor's cancellation/deadline so an in-flight read
420
+ // cannot outlive its coordinator quarantine unnoticed.
421
+ signal: context.acquisition.signal,
422
+ ...(context.acquisition.timing.deadlineAt === undefined
423
+ ? {}
424
+ : { deadlineAt: context.acquisition.timing.deadlineAt })
425
+ }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(postconditionRequest.signal, postconditionRequest.deadlineAt, transactionTimeoutMs, "operation-send-postcondition"), true);
426
+ // Provider observers are one-shot reads. A missing/temporarily
427
+ // ambiguous delta is a retryable observation miss; no browser
428
+ // mutation is ever retried here. Definitive blockers stay terminal.
429
+ if (isPlainPostconditionResult(probe)
430
+ && probe.status === "blocked"
431
+ && (probe.blockerCode === "target_evidence_unavailable" || probe.blockerCode === "ambiguous_submit")) {
432
+ return { result: probe, retryable: true };
433
+ }
434
+ return probe;
435
+ },
436
+ ...(baseObservers.sleep === undefined ? {} : { sleep: baseObservers.sleep }),
437
+ ...(baseObservers.maxPostconditionAttempts === undefined ? {} : { maxPostconditionAttempts: baseObservers.maxPostconditionAttempts }),
438
+ ...(baseObservers.postconditionIntervalMs === undefined ? {} : { postconditionIntervalMs: baseObservers.postconditionIntervalMs }),
439
+ ...(baseObservers.postconditionTimeoutMs === undefined ? {} : { postconditionTimeoutMs: baseObservers.postconditionTimeoutMs })
440
+ };
441
+ const operationController = new AbortController();
442
+ const sourceAbortCleanup = [];
443
+ if (request.signal !== undefined) {
444
+ const abortFromCaller = () => operationController.abort(request.signal?.reason);
445
+ if (request.signal.aborted)
446
+ operationController.abort(request.signal.reason);
447
+ else {
448
+ request.signal.addEventListener("abort", abortFromCaller, { once: true });
449
+ sourceAbortCleanup.push(() => request.signal?.removeEventListener("abort", abortFromCaller));
450
+ }
451
+ }
452
+ try {
453
+ const transaction = async (callback) => await binding.withTabTransaction(boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, request.mode === "mutate_once" ? "operation-send" : "operation-send-observe", request.mode === "mutate_once" ? "mutation" : "read"), async (context) => {
454
+ const abortFromCoordinator = () => operationController.abort(context.acquisition.signal.reason);
455
+ if (context.acquisition.signal.aborted)
456
+ operationController.abort(context.acquisition.signal.reason);
457
+ else
458
+ context.acquisition.signal.addEventListener("abort", abortFromCoordinator, { once: true });
459
+ try {
460
+ const current = await readCurrentTarget(binding, request.operationId, context.page, options.observeCurrentTarget, options.evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
461
+ binding.assertCurrent(current.evidence, current.authoritativeClaim);
462
+ return await callback();
463
+ }
464
+ finally {
465
+ context.acquisition.signal.removeEventListener("abort", abortFromCoordinator);
466
+ }
467
+ });
468
+ const result = await runSendOnce({
469
+ page,
470
+ operationId: request.operationId,
471
+ requestDigest: request.requestDigest,
472
+ surface: request.surface,
473
+ actionId: request.actionId,
474
+ mode: request.mode,
475
+ expected: request.expected,
476
+ observers,
477
+ signal: operationController.signal,
478
+ ...(request.deadlineAt === undefined ? { deadlineAt: MAX_DEADLINE_AT } : { deadlineAt: request.deadlineAt }),
479
+ ...(request.persistPreSendBaseline === undefined
480
+ ? {}
481
+ : { persistPreSendBaseline: request.persistPreSendBaseline }),
482
+ ...(request.durableBaseline === undefined
483
+ ? {}
484
+ : { durableBaseline: request.durableBaseline }),
485
+ transaction
486
+ });
487
+ if (binding.markTargetEstablished !== undefined
488
+ && (result.status === "submitted" || result.status === "already_submitted")
489
+ && result.targetEstablishment !== undefined) {
490
+ binding.markTargetEstablished({
491
+ conversationId: result.targetEstablishment.conversationId,
492
+ canonicalThreadUrl: result.targetEstablishment.canonicalThreadUrl
493
+ });
494
+ }
495
+ return result;
496
+ }
497
+ catch {
498
+ return request.mode === "mutate_once"
499
+ ? { status: "uncertain", quarantine: "provider" }
500
+ : { status: "blocked", blockerCode: "target_evidence_unavailable" };
501
+ }
502
+ finally {
503
+ for (const cleanup of sourceAbortCleanup)
504
+ cleanup();
505
+ }
506
+ };
507
+ const readContext = async (request) => {
508
+ if (options.recovery !== undefined) {
509
+ await ensureRecovered(request.operationId, request.requestDigest, options.recovery.surface, request.signal);
510
+ }
511
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
512
+ if (binding === undefined)
513
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
514
+ if (options.collector?.readContext === undefined) {
515
+ throw new OperationBrowserAdapterError("unsupported_browser_primitive");
516
+ }
517
+ try {
518
+ const context = await runReadTransaction(binding, request.operationId, transaction => options.collector.readContext(request, transaction.page, transaction.target), options.observeCurrentTarget, options.evidenceDigest, {
519
+ signal: request.signal,
520
+ timeoutMs: transactionTimeoutMs,
521
+ label: "operation-collect-context"
522
+ });
523
+ collectorContexts.set(request.operationId, context);
524
+ return context;
525
+ }
526
+ catch {
527
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
528
+ }
529
+ };
530
+ const observe = async (request) => {
531
+ if (options.recovery !== undefined) {
532
+ await ensureRecovered(request.operationId, request.requestDigest, options.recovery.surface, request.signal);
533
+ }
534
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
535
+ const context = collectorContexts.get(request.operationId);
536
+ if (binding === undefined || context === undefined)
537
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
538
+ try {
539
+ return await binding.withTabTransaction(boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-collect-observe"), async (transaction) => {
540
+ const current = await readCurrentTarget(binding, request.operationId, transaction.page, options.observeCurrentTarget, options.evidenceDigest, transaction.acquisition.signal, transaction.acquisition.timing.deadlineAt);
541
+ binding.assertCurrent(current.evidence, current.authoritativeClaim);
542
+ const observation = options.collector?.observe === undefined
543
+ ? await defaultCollectorObservation(request, transaction, context, options.evidenceDigest, current.authoritativeClaim)
544
+ : await options.collector.observe(request, transaction.page, transaction.target, context);
545
+ // The observation itself carries the exact target. Do not use a
546
+ // page-wide latest turn or a prompt similarity fallback.
547
+ binding.assertCurrent(observation.snapshot.target, current.authoritativeClaim);
548
+ return observation;
549
+ });
550
+ }
551
+ catch {
552
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
553
+ }
554
+ };
555
+ const sleep = async (milliseconds, signal) => {
556
+ if (options.collector?.sleep !== undefined)
557
+ return await options.collector.sleep(milliseconds, signal);
558
+ await sleepOutsideCoordinator(milliseconds, signal);
559
+ };
560
+ const observeTurn = async (request) => {
561
+ if (options.recovery !== undefined) {
562
+ await ensureRecovered(request.operationId, request.parentRequestDigest, options.recovery.surface, request.signal);
563
+ }
564
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
565
+ if (binding === undefined)
566
+ return { status: "uncertain", reason: "target_mismatch" };
567
+ if (options.control?.observeTurn === undefined)
568
+ return { status: "uncertain", reason: "unavailable" };
569
+ try {
570
+ return await runReadTransaction(binding, request.operationId, transaction => options.control.observeTurn(request, transaction.page, transaction.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-observe"));
571
+ }
572
+ catch {
573
+ return { status: "uncertain", reason: "unavailable" };
574
+ }
575
+ };
576
+ const executeControlOnce = async (request) => {
577
+ if (options.recovery !== undefined) {
578
+ await ensureRecovered(request.operationId, request.parentRequestDigest, options.recovery.surface, request.signal);
579
+ }
580
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
581
+ if (binding === undefined)
582
+ return { status: "uncertain", blockerCode: "target_binding_mismatch" };
583
+ if (options.control?.executeOnce === undefined)
584
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
585
+ try {
586
+ return await binding.withTabTransaction(boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, `operation-${request.action}`, "control"), async (transaction) => await (async () => {
587
+ const current = await readCurrentTarget(binding, request.operationId, transaction.page, options.observeCurrentTarget, options.evidenceDigest, transaction.acquisition.signal, transaction.acquisition.timing.deadlineAt);
588
+ binding.assertCurrent(current.evidence, current.authoritativeClaim);
589
+ return await options.control.executeOnce(request, transaction.page, transaction.target);
590
+ })());
591
+ }
592
+ catch {
593
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
594
+ }
595
+ };
596
+ const observePostcondition = async (request) => {
597
+ if (options.recovery !== undefined) {
598
+ await ensureRecovered(request.operationId, request.parentRequestDigest, options.recovery.surface, request.signal);
599
+ }
600
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
601
+ if (binding === undefined)
602
+ return { status: "uncertain", blockerCode: "target_binding_mismatch" };
603
+ if (options.control?.observePostcondition === undefined)
604
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
605
+ try {
606
+ return await runReadTransaction(binding, request.operationId, transaction => options.control.observePostcondition(request, transaction.page, transaction.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-postcondition"));
607
+ }
608
+ catch {
609
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
610
+ }
611
+ };
612
+ const getSteerBinding = async (operationId, requestDigest, signal) => await steerBindingFor(bindings, ensureRecovered, options.recovery, operationId, requestDigest, signal);
613
+ /**
614
+ * Work steer is deliberately separate from Stop's compatibility port. A
615
+ * preparation is one short read transaction and returns only the provider's
616
+ * prompt-free prepared capability. The caller persists that capability and
617
+ * its baseline before it can invoke executeSteerPrepared.
618
+ */
619
+ const prepareSteer = async (request) => {
620
+ const binding = await getSteerBinding(request.parentOperationId, request.parentRequestDigest, request.signal);
621
+ if (binding === undefined)
622
+ return blockedSteerPhase(request, "prepare", "target_binding_mismatch", false, "none");
623
+ const provider = options.control?.prepareSteer;
624
+ if (provider === undefined)
625
+ return blockedSteerPhase(request, "prepare", "backend_unavailable", true, "none");
626
+ try {
627
+ const result = await runReadTransaction(binding, request.parentOperationId, context => provider({
628
+ schemaVersion: request.schemaVersion,
629
+ parentOperationId: request.parentOperationId,
630
+ parentRequestDigest: request.parentRequestDigest,
631
+ parentTargetBindingDigest: request.parentTargetBindingDigest,
632
+ controlActionId: request.controlActionId,
633
+ requestDigest: request.requestDigest,
634
+ expectedAssistantTurnId: request.expectedAssistantTurnId,
635
+ signal: context.acquisition.signal,
636
+ deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
637
+ }, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-steer-prepare"));
638
+ return normalizeSteerProviderResult(result, request, "prepare");
639
+ }
640
+ catch (error) {
641
+ return blockedSteerPhase(request, "prepare", phaseFailureCode(request.signal, request.deadlineAt, error), true, "none");
642
+ }
643
+ };
644
+ /**
645
+ * Execute is the only Work-steer mutation site. `runSteerControlTransaction`
646
+ * keeps the callback promise alive until provider settlement even when the
647
+ * coordinator has already rejected the public transaction at its deadline.
648
+ * That quarantine is what prevents a late fill/click from overlapping a
649
+ * subsequent operation on the shared page.
650
+ */
651
+ const executeSteerPrepared = async (request) => {
652
+ const operationId = request.prepared.parentOperationId;
653
+ const binding = await getSteerBinding(operationId, request.prepared.parentRequestDigest, request.signal);
654
+ if (binding === undefined)
655
+ return blockedSteerPhase(request, "execute_prepared", "target_binding_mismatch", false, "none");
656
+ const provider = options.control?.executeSteerPrepared;
657
+ if (provider === undefined)
658
+ return blockedSteerPhase(request, "execute_prepared", "backend_unavailable", false, "none", request.prepared);
659
+ try {
660
+ const result = await runSteerControlTransaction(binding, operationId, request.signal, request.deadlineAt, transactionTimeoutMs, options.observeCurrentTarget, options.evidenceDigest, context => provider({
661
+ schemaVersion: request.schemaVersion,
662
+ prepared: request.prepared,
663
+ signal: context.acquisition.signal,
664
+ deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
665
+ }, context.page, context.target));
666
+ return normalizeSteerProviderResult(result, request, "execute_prepared", request.prepared);
667
+ }
668
+ catch (error) {
669
+ return uncertainSteerPhase(request, "execute_prepared", request.prepared, phaseFailureCode(request.signal, request.deadlineAt, error), "provider");
670
+ }
671
+ };
672
+ /** Verification is a bounded read and releases the tab actor before any
673
+ * caller-level retry or persistence work. */
674
+ const verifySteer = async (request) => {
675
+ const operationId = request.prepared.parentOperationId;
676
+ const binding = await getSteerBinding(operationId, request.prepared.parentRequestDigest, request.signal);
677
+ if (binding === undefined)
678
+ return uncertainSteerPhase(request, "verify", request.prepared, "target_binding_mismatch", "caller");
679
+ const provider = options.control?.verifySteer;
680
+ if (provider === undefined)
681
+ return uncertainSteerPhase(request, "verify", request.prepared, "backend_unavailable", "provider");
682
+ try {
683
+ const result = await runReadTransaction(binding, operationId, context => provider({
684
+ schemaVersion: request.schemaVersion,
685
+ prepared: request.prepared,
686
+ signal: context.acquisition.signal,
687
+ deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
688
+ }, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-steer-verify"));
689
+ return normalizeSteerProviderResult(result, request, "verify", request.prepared);
690
+ }
691
+ catch (error) {
692
+ return uncertainSteerPhase(request, "verify", request.prepared, phaseFailureCode(request.signal, request.deadlineAt, error), "caller");
693
+ }
694
+ };
695
+ /**
696
+ * Recovery is observation-only. It may hydrate a durable target binding,
697
+ * but it never calls prepare, execute, composer fill, or Send/click.
698
+ */
699
+ const recoverSteer = async (request) => {
700
+ const operationId = request.prepared.parentOperationId;
701
+ const binding = await getSteerBinding(operationId, request.prepared.parentRequestDigest, request.signal);
702
+ if (binding === undefined)
703
+ return uncertainSteerPhase(request, "recovery", request.prepared, "target_binding_mismatch", "caller");
704
+ const provider = options.control?.recoverSteer;
705
+ if (provider === undefined)
706
+ return uncertainSteerPhase(request, "recovery", request.prepared, "backend_unavailable", "provider");
707
+ try {
708
+ const result = await runReadTransaction(binding, operationId, context => provider({
709
+ schemaVersion: request.schemaVersion,
710
+ prepared: request.prepared,
711
+ baseline: request.baseline,
712
+ signal: context.acquisition.signal,
713
+ deadlineAt: context.acquisition.timing.deadlineAt ?? request.deadlineAt
714
+ }, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-control-steer-recovery"));
715
+ return normalizeSteerProviderResult(result, request, "recovery", request.prepared);
716
+ }
717
+ catch (error) {
718
+ return uncertainSteerPhase(request, "recovery", request.prepared, phaseFailureCode(request.signal, request.deadlineAt, error), "caller");
719
+ }
720
+ };
721
+ const submission = Object.freeze({
722
+ observeStaging,
723
+ executeFileHandoffOnce,
724
+ observeAttachments,
725
+ prepareSend,
726
+ executePreparedSend,
727
+ verifyPreparedSend,
728
+ recoverSend,
729
+ executeFinalTabTransaction
730
+ });
731
+ const collector = Object.freeze({ readContext, observe, sleep });
732
+ const control = options.control === undefined
733
+ ? undefined
734
+ : Object.freeze({
735
+ observeTurn,
736
+ executeOnce: executeControlOnce,
737
+ observePostcondition,
738
+ postconditionRetry: CONTROL_POSTCONDITION_RETRY_POLICY,
739
+ prepareSteer,
740
+ executeSteerPrepared,
741
+ verifySteer,
742
+ recoverSteer
743
+ });
744
+ const staging = options.staging === undefined
745
+ ? undefined
746
+ : Object.freeze({
747
+ readCurrent: async (callbackRequest) => {
748
+ const binding = bindings.get(callbackRequest.operationId);
749
+ if (binding === undefined)
750
+ return unavailableStagingObservation(callbackRequest);
751
+ return await runReadTransaction(binding, callbackRequest.operationId, transaction => options.staging.readCurrent === undefined
752
+ ? unavailableStagingObservation(callbackRequest)
753
+ : options.staging.readCurrent({ ...callbackRequest, page: transaction.page, target: transaction.target }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(callbackRequest.signal, callbackRequest.deadlineAt, transactionTimeoutMs, "operation-staging-read"));
754
+ },
755
+ observe: async (callbackRequest) => {
756
+ const binding = bindings.get(callbackRequest.operationId);
757
+ if (binding === undefined)
758
+ return unavailableStagingObservation(callbackRequest);
759
+ return await runReadTransaction(binding, callbackRequest.operationId, transaction => options.staging.observe === undefined
760
+ ? unavailableStagingObservation(callbackRequest)
761
+ : options.staging.observe({ ...callbackRequest, page: transaction.page, target: transaction.target }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(callbackRequest.signal, callbackRequest.deadlineAt, transactionTimeoutMs, "operation-staging-observe"));
762
+ },
763
+ mutateOnce: async (callbackRequest) => {
764
+ const binding = bindings.get(callbackRequest.operationId);
765
+ if (binding === undefined)
766
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
767
+ return await runMutationTransaction(binding, callbackRequest.operationId, transaction => options.staging.mutateOnce === undefined
768
+ ? Promise.reject(new OperationBrowserAdapterError("unsupported_browser_primitive"))
769
+ : options.staging.mutateOnce({ ...callbackRequest, page: transaction.page, target: transaction.target }), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(callbackRequest.signal, callbackRequest.deadlineAt, transactionTimeoutMs, "operation-staging-mutate", "mutation"));
770
+ }
771
+ });
772
+ const artifacts = options.artifacts === undefined || options.outputDirectory === undefined
773
+ ? undefined
774
+ : Object.freeze({
775
+ transfer: async (request) => {
776
+ const binding = bindingFor(bindings, request.operationId, request.targetBindingDigest);
777
+ if (binding === undefined) {
778
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
779
+ }
780
+ // `acquireDownload` is the only browser phase. Its promise is fully
781
+ // settled before this callback invokes `materializeDownload`, so a
782
+ // large stream or filesystem commit never owns the tab actor.
783
+ return await transferOperationArtifact({
784
+ operationId: request.operationId,
785
+ requestDigest: request.requestDigest,
786
+ targetBindingDigest: request.targetBindingDigest,
787
+ assistantTurnId: request.assistantTurnId,
788
+ sourceIdentityDigest: request.sourceIdentityDigest,
789
+ kind: request.kind,
790
+ ordinal: request.ordinal,
791
+ transferActionId: request.transferActionId,
792
+ outputDirectory: options.outputDirectory,
793
+ evidenceDigest: options.evidenceDigest,
794
+ signal: request.signal,
795
+ deadlineAt: request.deadlineAt,
796
+ ...(request.mimeTypeHint === undefined ? {} : { mimeTypeHint: request.mimeTypeHint }),
797
+ openSource: async (sourceRequest) => {
798
+ const download = await runMutationTransaction(binding, request.operationId, context => options.artifacts.acquireDownload({
799
+ ...sourceRequest,
800
+ signal: request.signal,
801
+ deadlineAt: request.deadlineAt
802
+ }, context.page, context.target), options.observeCurrentTarget, options.evidenceDigest, boundedRequest(request.signal, request.deadlineAt, transactionTimeoutMs, "operation-artifact-acquire", "mutation"));
803
+ // The transaction above has released the actor. This local
804
+ // phase must never call into the page or browser bridge.
805
+ return await options.artifacts.materializeDownload(download);
806
+ },
807
+ journal: request.journal
808
+ });
809
+ }
810
+ });
811
+ const adapter = Object.freeze({
812
+ resolveTarget,
813
+ submission,
814
+ collector,
815
+ ...(artifacts === undefined ? {} : { artifacts }),
816
+ ...(staging === undefined ? {} : { staging }),
817
+ ...(control === undefined ? {} : { control }),
818
+ });
819
+ return adapter;
820
+ }
821
+ function normalizeAdapterOptions(value) {
822
+ if (!isPlainDataRecord(value))
823
+ throw new OperationBrowserAdapterError("adapter_incomplete");
824
+ try {
825
+ const page = readOwnData(value, "page");
826
+ const runtimeContext = readOwnData(value, "runtimeContext");
827
+ const owner = readOwnData(value, "owner");
828
+ const coordinator = readOwnData(value, "coordinator");
829
+ const evidenceDigest = readOwnData(value, "evidenceDigest");
830
+ const targetEvidence = readOwnData(value, "targetEvidence");
831
+ const newTargetAnchorDigest = readOwnData(value, "newTargetAnchorDigest");
832
+ const blankTaskEvidenceDigest = readOwnData(value, "blankTaskEvidenceDigest");
833
+ const resolveTargetEvidence = readOwnData(value, "resolveTargetEvidence");
834
+ const observeCurrentTarget = readOwnData(value, "observeCurrentTarget");
835
+ const capabilities = readOwnData(value, "capabilities");
836
+ const authoritativeClaim = readOwnData(value, "authoritativeClaim");
837
+ const transactionTimeoutMs = readOwnData(value, "transactionTimeoutMs");
838
+ const files = readOwnData(value, "files");
839
+ const fileManifestDigest = readOwnData(value, "fileManifestDigest");
840
+ const submission = readOwnData(value, "submission");
841
+ const staging = readOwnData(value, "staging");
842
+ const collector = readOwnData(value, "collector");
843
+ const control = readOwnData(value, "control");
844
+ const artifacts = readOwnData(value, "artifacts");
845
+ const outputDirectory = readOwnData(value, "outputDirectory");
846
+ const recovery = readOwnData(value, "recovery");
847
+ const normalizedRecovery = recovery === undefined
848
+ ? undefined
849
+ : (() => {
850
+ validateRecoveryContext(recovery);
851
+ return normalizeRecoveryContext(recovery);
852
+ })();
853
+ const snapshot = {
854
+ page,
855
+ runtimeContext,
856
+ owner: cloneFrozenData(owner),
857
+ coordinator,
858
+ evidenceDigest,
859
+ targetEvidence: targetEvidence === undefined ? undefined : cloneFrozenData(targetEvidence),
860
+ newTargetAnchorDigest,
861
+ blankTaskEvidenceDigest,
862
+ resolveTargetEvidence,
863
+ observeCurrentTarget,
864
+ capabilities: capabilities === undefined ? undefined : cloneFrozenData(capabilities),
865
+ authoritativeClaim: authoritativeClaim === undefined ? undefined : cloneFrozenData(authoritativeClaim),
866
+ transactionTimeoutMs,
867
+ files: files === undefined ? undefined : cloneFrozenData(files),
868
+ fileManifestDigest,
869
+ submission: submission === undefined ? undefined : cloneFrozenProviderValue(submission),
870
+ staging: staging === undefined ? undefined : cloneFrozenProviderValue(staging),
871
+ collector: collector === undefined ? undefined : cloneFrozenProviderValue(collector),
872
+ control: control === undefined ? undefined : cloneFrozenProviderValue(control),
873
+ artifacts: artifacts === undefined ? undefined : cloneFrozenProviderValue(artifacts),
874
+ outputDirectory,
875
+ recovery: normalizedRecovery
876
+ };
877
+ const normalized = Object.freeze(snapshot);
878
+ validateOptions(normalized);
879
+ return normalized;
880
+ }
881
+ catch (error) {
882
+ if (error instanceof OperationBrowserAdapterError)
883
+ throw error;
884
+ throw new OperationBrowserAdapterError("adapter_incomplete");
885
+ }
886
+ }
887
+ function validateOptions(options) {
888
+ if (!isPlainDataRecord(options) || options.page === null || typeof options.page !== "object") {
889
+ throw new OperationBrowserAdapterError("adapter_incomplete");
890
+ }
891
+ if (typeof options.evidenceDigest !== "function")
892
+ throw new OperationBrowserAdapterError("adapter_incomplete");
893
+ if (!options.owner || typeof options.owner.backendSessionId !== "string")
894
+ throw new OperationBrowserAdapterError("adapter_incomplete");
895
+ if (options.runtimeContext !== undefined && !(options.runtimeContext instanceof OperationRuntimeContext)) {
896
+ throw new OperationBrowserAdapterError("adapter_incomplete");
897
+ }
898
+ if (options.recovery !== undefined) {
899
+ validateRecoveryContext(options.recovery);
900
+ if (options.observeCurrentTarget === undefined) {
901
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
902
+ }
903
+ }
904
+ else if (options.targetEvidence === undefined && options.resolveTargetEvidence === undefined) {
905
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
906
+ }
907
+ if (options.files !== undefined && !Array.isArray(options.files))
908
+ throw new OperationBrowserAdapterError("adapter_incomplete");
909
+ if (options.recovery !== undefined
910
+ && (options.artifacts !== undefined || options.outputDirectory !== undefined)) {
911
+ throw new OperationBrowserAdapterError("adapter_incomplete");
912
+ }
913
+ if (options.artifacts !== undefined) {
914
+ if (!isPlainDataRecord(options.artifacts)
915
+ || typeof readOwnData(options.artifacts, "acquireDownload") !== "function"
916
+ || typeof readOwnData(options.artifacts, "materializeDownload") !== "function") {
917
+ throw new OperationBrowserAdapterError("adapter_incomplete");
918
+ }
919
+ }
920
+ if (options.outputDirectory !== undefined
921
+ && (typeof options.outputDirectory !== "string"
922
+ || options.outputDirectory.length === 0
923
+ || options.outputDirectory.length > 4096
924
+ || !isAbsolutePath(options.outputDirectory)
925
+ || /[\u0000-\u001f\u007f]/u.test(options.outputDirectory))) {
926
+ throw new OperationBrowserAdapterError("adapter_incomplete");
927
+ }
928
+ if (options.transactionTimeoutMs !== undefined
929
+ && (!Number.isSafeInteger(options.transactionTimeoutMs) || options.transactionTimeoutMs < 1 || options.transactionTimeoutMs > MAX_TRANSACTION_TIMEOUT_MS)) {
930
+ throw new OperationBrowserAdapterError("adapter_incomplete");
931
+ }
932
+ }
933
+ function validateRecoveryContext(value) {
934
+ if (!isPlainDataRecord(value))
935
+ throw new OperationBrowserAdapterError("adapter_incomplete");
936
+ if (readOwnData(value, "operationId") === undefined
937
+ || readOwnData(value, "requestDigest") === undefined
938
+ || readOwnData(value, "surface") === undefined
939
+ || readOwnData(value, "target") === undefined
940
+ || readOwnData(value, "signal") === undefined) {
941
+ throw new OperationBrowserAdapterError("adapter_incomplete");
942
+ }
943
+ const operationId = readOwnData(value, "operationId");
944
+ const requestDigest = readOwnData(value, "requestDigest");
945
+ const surface = readOwnData(value, "surface");
946
+ const target = readOwnData(value, "target");
947
+ const signal = readOwnData(value, "signal");
948
+ if (typeof operationId !== "string"
949
+ || !OPAQUE_ID_PATTERN.test(operationId)
950
+ || typeof requestDigest !== "string"
951
+ || !DIGEST_PATTERN.test(requestDigest)
952
+ || (surface !== "chat" && surface !== "work")
953
+ || target === null
954
+ || typeof target !== "object"
955
+ || Array.isArray(target)) {
956
+ throw new OperationBrowserAdapterError("adapter_incomplete");
957
+ }
958
+ assertNativeAbortSignal(signal);
959
+ if (!isSafeDataGraph(target) || !isPlainDataRecord(target)) {
960
+ throw new OperationBrowserAdapterError("adapter_incomplete");
961
+ }
962
+ const lifecycle = readOwnData(target, "targetLifecycle");
963
+ if (lifecycle === "new_pending") {
964
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
965
+ }
966
+ if (lifecycle !== undefined && lifecycle !== "fixed" && lifecycle !== "new_established") {
967
+ throw new OperationBrowserAdapterError("adapter_incomplete");
968
+ }
969
+ const providerId = readOwnData(target, "providerId");
970
+ const browserId = readOwnData(target, "browserId");
971
+ const tabId = readOwnData(target, "tabId");
972
+ const coordinationScope = readOwnData(target, "coordinationScope");
973
+ const evidenceProfile = readOwnData(target, "evidenceProfile");
974
+ if (typeof providerId !== "string"
975
+ || !OPAQUE_ID_PATTERN.test(providerId)
976
+ || typeof browserId !== "string"
977
+ || !OPAQUE_ID_PATTERN.test(browserId)
978
+ || typeof tabId !== "string"
979
+ || !OPAQUE_ID_PATTERN.test(tabId)
980
+ || (coordinationScope !== "process" && coordinationScope !== "provider")
981
+ || !isPlainDataRecord(evidenceProfile)) {
982
+ throw new OperationBrowserAdapterError("adapter_incomplete");
983
+ }
984
+ const conversationId = readOwnData(target, "conversationId");
985
+ const canonicalThreadUrl = readOwnData(target, "canonicalThreadUrl");
986
+ if (typeof conversationId !== "string"
987
+ || !OPAQUE_ID_PATTERN.test(conversationId)
988
+ || typeof canonicalThreadUrl !== "string"
989
+ || !OPAQUE_THREAD_URL_PATTERN.test(canonicalThreadUrl)) {
990
+ throw new OperationBrowserAdapterError("adapter_incomplete");
991
+ }
992
+ if (lifecycle === "new_established" && !isPlainDataRecord(readOwnData(target, "targetEstablishment"))) {
993
+ throw new OperationBrowserAdapterError("adapter_incomplete");
994
+ }
995
+ const claimDigest = readOwnData(target, "tabClaimEvidenceDigest");
996
+ if (claimDigest !== undefined && (typeof claimDigest !== "string" || !DIGEST_PATTERN.test(claimDigest))) {
997
+ throw new OperationBrowserAdapterError("adapter_incomplete");
998
+ }
999
+ }
1000
+ function normalizeRecoveryContext(value) {
1001
+ try {
1002
+ const record = value;
1003
+ const operationId = readOwnData(record, "operationId");
1004
+ const requestDigest = readOwnData(record, "requestDigest");
1005
+ const surface = readOwnData(record, "surface");
1006
+ const targetValue = readOwnData(record, "target");
1007
+ const signal = readOwnData(record, "signal");
1008
+ if (targetValue === undefined || signal === undefined)
1009
+ throw new Error("incomplete recovery context");
1010
+ const target = cloneFrozenData(targetValue);
1011
+ return Object.freeze({
1012
+ operationId: operationId,
1013
+ requestDigest: requestDigest,
1014
+ surface: surface,
1015
+ target,
1016
+ signal: signal
1017
+ });
1018
+ }
1019
+ catch {
1020
+ throw new OperationBrowserAdapterError("adapter_incomplete");
1021
+ }
1022
+ }
1023
+ async function hydrateRecoveredTarget(options, recovery, page, coordinator) {
1024
+ const observeCurrentTarget = options.observeCurrentTarget;
1025
+ if (observeCurrentTarget === undefined) {
1026
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1027
+ }
1028
+ assertNativeAbortSignal(recovery.signal);
1029
+ let observed;
1030
+ try {
1031
+ observed = normalizeCurrentTargetResult(await observeCurrentTarget({
1032
+ page,
1033
+ operationId: recovery.operationId,
1034
+ target: recovery.target,
1035
+ signal: recovery.signal
1036
+ }));
1037
+ }
1038
+ catch (error) {
1039
+ if (error instanceof OperationBrowserAdapterError)
1040
+ throw error;
1041
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1042
+ }
1043
+ assertRecoveredTargetIdentity(recovery.target, observed, options);
1044
+ const lifecycle = recovery.target.targetLifecycle ?? "fixed";
1045
+ let bound;
1046
+ try {
1047
+ bound = bindBrowserTarget({
1048
+ page,
1049
+ evidence: observed.evidence,
1050
+ ...(lifecycle === "new_established" ? { targetLifecycle: "new_established" } : {}),
1051
+ ...(recovery.target.coordinationScope !== "provider" || observed.authoritativeClaim === undefined
1052
+ ? {}
1053
+ : { authoritativeClaim: observed.authoritativeClaim }),
1054
+ ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities }),
1055
+ evidenceDigest: options.evidenceDigest,
1056
+ owner: Object.freeze({ ...options.owner, operationId: recovery.operationId }),
1057
+ coordinator,
1058
+ ...(recovery.target.userTurnBaselineDigest === undefined ? {} : { userTurnBaselineDigest: recovery.target.userTurnBaselineDigest }),
1059
+ ...(recovery.target.assistantTurnBaselineDigest === undefined ? {} : { assistantTurnBaselineDigest: recovery.target.assistantTurnBaselineDigest }),
1060
+ ...(recovery.target.configurationReceiptDigest === undefined ? {} : { configurationReceiptDigest: recovery.target.configurationReceiptDigest })
1061
+ });
1062
+ }
1063
+ catch (error) {
1064
+ if (error instanceof OperationBrowserAdapterError)
1065
+ throw error;
1066
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
1067
+ }
1068
+ return preserveRecoveredTarget(bound, recovery.target);
1069
+ }
1070
+ function assertRecoveredTargetIdentity(target, observed, options) {
1071
+ const evidence = observed.evidence;
1072
+ compareRecoveredIdentity(evidence.provider, target.providerId);
1073
+ compareRecoveredIdentity(evidence.browser, target.browserId);
1074
+ compareRecoveredIdentity(evidence.tab, target.tabId);
1075
+ compareRecoveredIdentity(evidence.conversation, target.conversationId);
1076
+ compareRecoveredIdentity(evidence.canonicalThreadUrl, target.canonicalThreadUrl);
1077
+ if (target.coordinationScope !== "provider")
1078
+ return;
1079
+ const claim = observed.authoritativeClaim;
1080
+ if (claim === undefined
1081
+ || target.tabClaimEvidenceDigest === undefined
1082
+ || options.capabilities?.stableProviderId !== true
1083
+ || options.capabilities.stableBrowserId !== true
1084
+ || options.capabilities.stableTabId !== true
1085
+ || options.capabilities.concurrentTabs !== true
1086
+ || options.capabilities.authoritativeTabClaim !== true) {
1087
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
1088
+ }
1089
+ let claimDigest;
1090
+ try {
1091
+ claimDigest = options.evidenceDigest(CLAIM_EVIDENCE_DIGEST_DOMAIN, {
1092
+ token: claim.token,
1093
+ epoch: claim.epoch
1094
+ });
1095
+ }
1096
+ catch {
1097
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
1098
+ }
1099
+ if (claimDigest !== target.tabClaimEvidenceDigest) {
1100
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
1101
+ }
1102
+ }
1103
+ function compareRecoveredIdentity(observed, expected) {
1104
+ if (expected === undefined || observed.status !== "available" || observed.value !== expected) {
1105
+ throw new OperationBrowserAdapterError("target_binding_mismatch");
1106
+ }
1107
+ }
1108
+ function preserveRecoveredTarget(binding, target) {
1109
+ const assertCurrent = (evidence, claim, allowNewTargetEstablishment = false) => {
1110
+ binding.assertCurrent(evidence, claim, allowNewTargetEstablishment);
1111
+ compareRecoveredIdentity(evidence.provider, target.providerId);
1112
+ compareRecoveredIdentity(evidence.browser, target.browserId);
1113
+ compareRecoveredIdentity(evidence.tab, target.tabId);
1114
+ compareRecoveredIdentity(evidence.conversation, target.conversationId);
1115
+ compareRecoveredIdentity(evidence.canonicalThreadUrl, target.canonicalThreadUrl);
1116
+ };
1117
+ return Object.freeze({
1118
+ page: binding.page,
1119
+ target,
1120
+ targetEvidenceDigest: binding.targetEvidenceDigest,
1121
+ evidence: binding.evidence,
1122
+ capabilities: binding.capabilities,
1123
+ resource: binding.resource,
1124
+ owner: binding.owner,
1125
+ assertPage: binding.assertPage,
1126
+ assertCurrent,
1127
+ withTabTransaction: (transactionOptions, callback) => binding.withTabTransaction(transactionOptions, context => callback(Object.freeze({
1128
+ ...context,
1129
+ target,
1130
+ assertCurrent
1131
+ })))
1132
+ });
1133
+ }
1134
+ function cloneFrozenData(value, seen = new Set()) {
1135
+ if (value === null || typeof value !== "object") {
1136
+ if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
1137
+ throw new Error("unsupported recovery value");
1138
+ }
1139
+ return value;
1140
+ }
1141
+ if (seen.has(value))
1142
+ throw new Error("cyclic recovery value");
1143
+ seen.add(value);
1144
+ try {
1145
+ if (Array.isArray(value)) {
1146
+ const result = value.map(item => cloneFrozenData(item, seen));
1147
+ seen.delete(value);
1148
+ return Object.freeze(result);
1149
+ }
1150
+ const prototype = Object.getPrototypeOf(value);
1151
+ if (prototype !== Object.prototype && prototype !== null)
1152
+ throw new Error("non-plain recovery value");
1153
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1154
+ // Use a null-prototype destination and explicit definitions so an own
1155
+ // `__proto__` data key cannot invoke the legacy setter and disappear.
1156
+ const result = Object.create(null);
1157
+ for (const key of Object.keys(descriptors)) {
1158
+ const descriptor = descriptors[key];
1159
+ if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
1160
+ throw new Error("accessor recovery value");
1161
+ }
1162
+ Object.defineProperty(result, key, {
1163
+ value: cloneFrozenData(descriptor.value, seen),
1164
+ enumerable: descriptor.enumerable ?? false,
1165
+ writable: true,
1166
+ configurable: true
1167
+ });
1168
+ }
1169
+ seen.delete(value);
1170
+ return Object.freeze(result);
1171
+ }
1172
+ catch (error) {
1173
+ seen.delete(value);
1174
+ throw error;
1175
+ }
1176
+ }
1177
+ /** Clone provider primitive records without ever reading an accessor/get trap. */
1178
+ function cloneFrozenProviderValue(value, seen = new Set()) {
1179
+ if (value === null || typeof value !== "object")
1180
+ return value;
1181
+ if (seen.has(value))
1182
+ throw new Error("cyclic provider value");
1183
+ seen.add(value);
1184
+ try {
1185
+ if (Array.isArray(value)) {
1186
+ const result = value.map(item => cloneFrozenProviderValue(item, seen));
1187
+ seen.delete(value);
1188
+ return Object.freeze(result);
1189
+ }
1190
+ const prototype = Object.getPrototypeOf(value);
1191
+ if (prototype !== Object.prototype && prototype !== null)
1192
+ throw new Error("non-plain provider value");
1193
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1194
+ const result = Object.create(null);
1195
+ for (const key of Object.keys(descriptors)) {
1196
+ const descriptor = descriptors[key];
1197
+ if (descriptor === undefined || !("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined) {
1198
+ throw new Error("accessor provider value");
1199
+ }
1200
+ Object.defineProperty(result, key, {
1201
+ value: cloneFrozenProviderValue(descriptor.value, seen),
1202
+ enumerable: descriptor.enumerable ?? false,
1203
+ writable: true,
1204
+ configurable: true
1205
+ });
1206
+ }
1207
+ seen.delete(value);
1208
+ return Object.freeze(result);
1209
+ }
1210
+ catch (error) {
1211
+ seen.delete(value);
1212
+ throw error;
1213
+ }
1214
+ }
1215
+ function assertNativeAbortSignal(value) {
1216
+ if (value === null || typeof value !== "object" || typeof AbortSignal !== "function") {
1217
+ throw new OperationBrowserAdapterError("adapter_incomplete");
1218
+ }
1219
+ const getter = Object.getOwnPropertyDescriptor(AbortSignal.prototype, "aborted")?.get;
1220
+ if (getter === undefined)
1221
+ throw new OperationBrowserAdapterError("adapter_incomplete");
1222
+ try {
1223
+ if (typeof Reflect.apply(getter, value, []) !== "boolean")
1224
+ throw new Error("invalid signal");
1225
+ }
1226
+ catch {
1227
+ throw new OperationBrowserAdapterError("adapter_incomplete");
1228
+ }
1229
+ }
1230
+ async function resolveProbe(options, request) {
1231
+ if (options.resolveTargetEvidence !== undefined) {
1232
+ const value = await options.resolveTargetEvidence({
1233
+ page: options.page,
1234
+ operationId: request.operationId,
1235
+ requestDigest: request.requestDigest,
1236
+ surface: request.surface,
1237
+ target: request.target,
1238
+ signal: request.signal
1239
+ });
1240
+ return normalizeTargetProbe(value);
1241
+ }
1242
+ if (options.targetEvidence === undefined)
1243
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1244
+ return normalizeTargetProbe(Object.freeze({
1245
+ page: options.page,
1246
+ evidence: options.targetEvidence,
1247
+ ...(options.newTargetAnchorDigest === undefined ? {} : { newTargetAnchorDigest: options.newTargetAnchorDigest }),
1248
+ ...(options.blankTaskEvidenceDigest === undefined ? {} : { blankTaskEvidenceDigest: options.blankTaskEvidenceDigest }),
1249
+ ...(options.authoritativeClaim === undefined ? {} : { authoritativeClaim: options.authoritativeClaim }),
1250
+ ...(options.capabilities === undefined ? {} : { capabilities: options.capabilities })
1251
+ }));
1252
+ }
1253
+ /**
1254
+ * Read resolver output through own data descriptors only. Provider adapters
1255
+ * are an untrusted boundary: a getter or proxy trap must not run while the
1256
+ * operation is deciding whether the Send target is safe.
1257
+ */
1258
+ function normalizeTargetProbe(value) {
1259
+ if (!isPlainDataRecord(value))
1260
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1261
+ const page = readOwnData(value, "page");
1262
+ const evidence = readOwnData(value, "evidence");
1263
+ const targetLifecycle = readOwnData(value, "targetLifecycle");
1264
+ const newTargetAnchorDigest = readOwnData(value, "newTargetAnchorDigest");
1265
+ const blankTaskEvidenceDigest = readOwnData(value, "blankTaskEvidenceDigest");
1266
+ const authoritativeClaim = readOwnData(value, "authoritativeClaim");
1267
+ const capabilities = readOwnData(value, "capabilities");
1268
+ const allowed = new Set([
1269
+ "page",
1270
+ "evidence",
1271
+ "targetLifecycle",
1272
+ "newTargetAnchorDigest",
1273
+ "blankTaskEvidenceDigest",
1274
+ "authoritativeClaim",
1275
+ "capabilities"
1276
+ ]);
1277
+ try {
1278
+ for (const key of Object.keys(value))
1279
+ if (!allowed.has(key))
1280
+ throw new Error("unsupported probe field");
1281
+ }
1282
+ catch {
1283
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1284
+ }
1285
+ if (evidence === undefined)
1286
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1287
+ if (!isSafeDataGraph(evidence) || (authoritativeClaim !== undefined && !isSafeDataGraph(authoritativeClaim)) || (capabilities !== undefined && !isSafeDataGraph(capabilities))) {
1288
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1289
+ }
1290
+ if (targetLifecycle !== undefined
1291
+ && targetLifecycle !== "fixed"
1292
+ && targetLifecycle !== "new_pending"
1293
+ && targetLifecycle !== "new_established") {
1294
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1295
+ }
1296
+ return Object.freeze({
1297
+ ...(page === undefined ? {} : { page: page }),
1298
+ evidence: evidence,
1299
+ ...(targetLifecycle === undefined ? {} : { targetLifecycle }),
1300
+ ...(newTargetAnchorDigest === undefined ? {} : { newTargetAnchorDigest: newTargetAnchorDigest }),
1301
+ ...(blankTaskEvidenceDigest === undefined ? {} : { blankTaskEvidenceDigest: blankTaskEvidenceDigest }),
1302
+ ...(authoritativeClaim === undefined ? {} : { authoritativeClaim: authoritativeClaim }),
1303
+ ...(capabilities === undefined ? {} : { capabilities: capabilities })
1304
+ });
1305
+ }
1306
+ function isPlainDataRecord(value) {
1307
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1308
+ return false;
1309
+ try {
1310
+ const prototype = Object.getPrototypeOf(value);
1311
+ if (prototype !== Object.prototype && prototype !== null)
1312
+ return false;
1313
+ for (const descriptor of Object.values(Object.getOwnPropertyDescriptors(value))) {
1314
+ if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
1315
+ return false;
1316
+ }
1317
+ return true;
1318
+ }
1319
+ catch {
1320
+ return false;
1321
+ }
1322
+ }
1323
+ function readOwnData(value, key) {
1324
+ try {
1325
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1326
+ return descriptor !== undefined && "value" in descriptor && descriptor.get === undefined && descriptor.set === undefined
1327
+ ? descriptor.value
1328
+ : undefined;
1329
+ }
1330
+ catch {
1331
+ return undefined;
1332
+ }
1333
+ }
1334
+ function isSafeDataGraph(value, seen = new Set(), depth = 0) {
1335
+ if (value === null || (typeof value !== "object" && typeof value !== "function"))
1336
+ return true;
1337
+ if (typeof value === "function")
1338
+ return true;
1339
+ if (depth > 16 || seen.has(value))
1340
+ return depth <= 16;
1341
+ seen.add(value);
1342
+ try {
1343
+ const prototype = Object.getPrototypeOf(value);
1344
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null)
1345
+ return false;
1346
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1347
+ if (Object.keys(descriptors).length > 1024)
1348
+ return false;
1349
+ for (const descriptor of Object.values(descriptors)) {
1350
+ if (!("value" in descriptor) || descriptor.get !== undefined || descriptor.set !== undefined)
1351
+ return false;
1352
+ if (!isSafeDataGraph(descriptor.value, seen, depth + 1))
1353
+ return false;
1354
+ }
1355
+ return true;
1356
+ }
1357
+ catch {
1358
+ return false;
1359
+ }
1360
+ }
1361
+ function assertProbePage(boundPage, probedPage) {
1362
+ if (probedPage !== undefined && probedPage !== boundPage)
1363
+ throw new OperationBrowserAdapterError("page_affinity_mismatch");
1364
+ }
1365
+ function assertRuntimeAffinity(runtimeCapture, page, probe) {
1366
+ const tabId = probe.evidence.tab.status === "available" ? probe.evidence.tab.value : undefined;
1367
+ const claim = probe.authoritativeClaim === undefined
1368
+ ? undefined
1369
+ : { status: "available", token: probe.authoritativeClaim.token, epoch: probe.authoritativeClaim.epoch };
1370
+ runtimeCapture.assertPageAffinity(page, {
1371
+ ...(tabId === undefined ? {} : { tabId }),
1372
+ ...(claim === undefined ? {} : { authoritativeClaim: claim })
1373
+ });
1374
+ }
1375
+ function assertStaticTarget(target, evidence, hasResolver) {
1376
+ if (hasResolver)
1377
+ return;
1378
+ switch (target.type) {
1379
+ case "selected_tab":
1380
+ return;
1381
+ case "tab_id":
1382
+ if (evidence.tab.status === "available" && evidence.tab.value === target.tabId)
1383
+ return;
1384
+ break;
1385
+ case "conversation_id":
1386
+ if (evidence.conversation.status === "available" && evidence.conversation.value === target.conversationId)
1387
+ return;
1388
+ break;
1389
+ case "new":
1390
+ case "url":
1391
+ break;
1392
+ }
1393
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1394
+ }
1395
+ function bindingFor(bindings, operationId, _targetBindingDigest) {
1396
+ const binding = bindings.get(operationId);
1397
+ // The journal owner computes the durable target-binding digest after the
1398
+ // target-bound event is appended. `bindBrowserTarget` also emits a local
1399
+ // evidence digest, but those domains intentionally differ. Operation ID is
1400
+ // therefore the only safe adapter lookup key here; OperationService has
1401
+ // already authenticated the caller handle before reaching these ports.
1402
+ if (binding === undefined)
1403
+ return undefined;
1404
+ return binding;
1405
+ }
1406
+ function unavailableStage(reason) {
1407
+ return { status: "unavailable", reason };
1408
+ }
1409
+ function unavailableStagingObservation(request) {
1410
+ return {
1411
+ status: "unavailable",
1412
+ desiredStateDigest: request.desiredStateDigest,
1413
+ blockerCode: "target_evidence_unavailable"
1414
+ };
1415
+ }
1416
+ function matchFileManifest(identities, manifestDigest, manifest) {
1417
+ if (manifest.count === 0)
1418
+ return [];
1419
+ if (identities === undefined || identities.length !== manifest.count || manifestDigest === undefined)
1420
+ return undefined;
1421
+ const sorted = [...identities];
1422
+ for (let ordinal = 0; ordinal < manifest.count; ordinal += 1) {
1423
+ const identity = sorted[ordinal];
1424
+ const expected = manifest.identities[ordinal];
1425
+ if (identity === undefined || expected === undefined || expected.ordinal !== ordinal)
1426
+ return undefined;
1427
+ let digest;
1428
+ try {
1429
+ digest = manifestDigest(ordinal, identity.manifest);
1430
+ }
1431
+ catch {
1432
+ return undefined;
1433
+ }
1434
+ if (digest !== expected.identityDigest || !DIGEST_PATTERN.test(digest))
1435
+ return undefined;
1436
+ }
1437
+ return Object.freeze(sorted);
1438
+ }
1439
+ async function runReadTransaction(binding, operationId, callback, observeCurrentTarget, evidenceDigest, transactionOptions = { timeoutMs: DEFAULT_TRANSACTION_TIMEOUT_MS }, allowNewTargetEstablishment = false) {
1440
+ return await binding.withTabTransaction({ ...transactionOptions, priority: transactionOptions.priority ?? "read", label: transactionOptions.label ?? "operation-read" }, async (context) => {
1441
+ const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
1442
+ binding.assertCurrent(current.evidence, current.authoritativeClaim, allowNewTargetEstablishment);
1443
+ return await callback(context);
1444
+ });
1445
+ }
1446
+ async function runMutationTransaction(binding, operationId, callback, observeCurrentTarget, evidenceDigest, transactionOptions = { timeoutMs: DEFAULT_TRANSACTION_TIMEOUT_MS }) {
1447
+ return await binding.withTabTransaction({ ...transactionOptions, priority: transactionOptions.priority ?? "mutation", label: transactionOptions.label ?? "operation-mutation" }, async (context) => {
1448
+ const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
1449
+ binding.assertCurrent(current.evidence, current.authoritativeClaim);
1450
+ return await callback(context);
1451
+ });
1452
+ }
1453
+ /** Resolve one durable identity from either a fresh or prepared phase call. */
1454
+ function steerPhaseIdentity(request) {
1455
+ if ("prepared" in request) {
1456
+ return {
1457
+ parentOperationId: request.prepared.parentOperationId,
1458
+ parentRequestDigest: request.prepared.parentRequestDigest,
1459
+ parentTargetBindingDigest: request.prepared.parentTargetBindingDigest,
1460
+ controlActionId: request.prepared.controlActionId,
1461
+ requestDigest: request.prepared.requestDigest,
1462
+ expectedAssistantTurnId: request.prepared.expectedAssistantTurnId
1463
+ };
1464
+ }
1465
+ return {
1466
+ parentOperationId: request.parentOperationId,
1467
+ parentRequestDigest: request.parentRequestDigest,
1468
+ parentTargetBindingDigest: request.parentTargetBindingDigest,
1469
+ controlActionId: request.controlActionId,
1470
+ requestDigest: request.requestDigest,
1471
+ expectedAssistantTurnId: request.expectedAssistantTurnId
1472
+ };
1473
+ }
1474
+ function steerPhaseBase(request, phase, prepared) {
1475
+ const identity = steerPhaseIdentity(request);
1476
+ const selected = prepared ?? ("prepared" in request ? request.prepared : undefined);
1477
+ return {
1478
+ schemaVersion: CONTROL_COORDINATOR_SCHEMA_VERSION,
1479
+ phase,
1480
+ parentOperationId: identity.parentOperationId,
1481
+ parentRequestDigest: identity.parentRequestDigest,
1482
+ parentTargetBindingDigest: identity.parentTargetBindingDigest,
1483
+ controlActionId: identity.controlActionId,
1484
+ action: "steer",
1485
+ requestDigest: identity.requestDigest,
1486
+ expectedAssistantTurnId: identity.expectedAssistantTurnId,
1487
+ ...(selected === undefined ? {} : {
1488
+ assistantBranchId: selected.assistantBranchId,
1489
+ assistantParentTurnId: selected.assistantParentTurnId,
1490
+ baselineSnapshotDigest: selected.baselineSnapshotDigest,
1491
+ preparedDigest: selected.preparedDigest
1492
+ })
1493
+ };
1494
+ }
1495
+ function blockedSteerPhase(request, phase, blockerCode, observationRequired, mutationBoundary, prepared, evidenceDigest) {
1496
+ return Object.freeze({
1497
+ ...steerPhaseBase(request, phase, prepared),
1498
+ status: "blocked",
1499
+ blockerCode,
1500
+ observationRequired,
1501
+ mutationBoundary,
1502
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
1503
+ });
1504
+ }
1505
+ function uncertainSteerPhase(request, phase, prepared, blockerCode, quarantine, evidenceDigest) {
1506
+ return Object.freeze({
1507
+ ...steerPhaseBase(request, phase, prepared),
1508
+ status: "uncertain",
1509
+ blockerCode,
1510
+ observationRequired: true,
1511
+ mutationBoundary: "control_may_have_occurred",
1512
+ quarantine,
1513
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
1514
+ });
1515
+ }
1516
+ /** Do not expose provider diagnostics or untrusted extra fields downstream. */
1517
+ function normalizeSteerProviderResult(value, request, phase, prepared) {
1518
+ const clone = cloneFrozenData(value);
1519
+ if (!isPlainDataRecord(clone))
1520
+ throw new Error("invalid steer phase result");
1521
+ const expected = steerPhaseBase(request, phase, prepared);
1522
+ for (const [key, expectedValue] of Object.entries(expected)) {
1523
+ if (readOwnData(clone, key) !== expectedValue)
1524
+ throw new Error("steer phase identity mismatch");
1525
+ }
1526
+ const status = readOwnData(clone, "status");
1527
+ const observationRequired = readOwnData(clone, "observationRequired");
1528
+ const mutationBoundary = readOwnData(clone, "mutationBoundary");
1529
+ if (typeof observationRequired !== "boolean"
1530
+ || (mutationBoundary !== "none" && mutationBoundary !== "control_may_have_occurred")) {
1531
+ throw new Error("invalid steer phase boundary");
1532
+ }
1533
+ const evidenceDigest = readOwnData(clone, "evidenceDigest");
1534
+ if (evidenceDigest !== undefined && (typeof evidenceDigest !== "string" || !DIGEST_PATTERN.test(evidenceDigest))) {
1535
+ throw new Error("invalid steer evidence");
1536
+ }
1537
+ const blockerCode = readOwnData(clone, "blockerCode");
1538
+ if (blockerCode !== undefined && (typeof blockerCode !== "string" || !/^[a-z][a-z0-9_]{0,63}$/u.test(blockerCode))) {
1539
+ throw new Error("invalid steer blocker");
1540
+ }
1541
+ if (status === "prepared") {
1542
+ if (phase !== "prepare" || observationRequired !== false || mutationBoundary !== "none")
1543
+ throw new Error("invalid steer preparation");
1544
+ const preparedValue = readOwnData(clone, "prepared");
1545
+ if (!isPlainDataRecord(preparedValue))
1546
+ throw new Error("missing steer preparation");
1547
+ assertSteerPreparedIdentity(preparedValue, steerPhaseIdentity(request));
1548
+ return Object.freeze({
1549
+ ...expected,
1550
+ status: "prepared",
1551
+ observationRequired: false,
1552
+ mutationBoundary: "none",
1553
+ prepared: cloneFrozenData(preparedValue)
1554
+ });
1555
+ }
1556
+ if (status === "executed") {
1557
+ if (phase !== "execute_prepared" || prepared === undefined || observationRequired !== true || mutationBoundary !== "control_may_have_occurred") {
1558
+ throw new Error("invalid steer execution");
1559
+ }
1560
+ return Object.freeze({
1561
+ ...expected,
1562
+ status: "executed",
1563
+ observationRequired: true,
1564
+ mutationBoundary: "control_may_have_occurred"
1565
+ });
1566
+ }
1567
+ if (status === "satisfied") {
1568
+ if ((phase !== "verify" && phase !== "recovery") || prepared === undefined || observationRequired !== false || mutationBoundary !== "control_may_have_occurred") {
1569
+ throw new Error("invalid steer satisfaction");
1570
+ }
1571
+ const receipt = readOwnData(clone, "receipt");
1572
+ if (!isPlainDataRecord(receipt))
1573
+ throw new Error("missing steer receipt");
1574
+ assertSteerReceiptIdentity(receipt, prepared);
1575
+ return Object.freeze({
1576
+ ...expected,
1577
+ status: "satisfied",
1578
+ observationRequired: false,
1579
+ mutationBoundary: "control_may_have_occurred",
1580
+ receipt: cloneFrozenData(receipt)
1581
+ });
1582
+ }
1583
+ if (status === "blocked") {
1584
+ if (typeof blockerCode !== "string" || readOwnData(clone, "quarantine") !== undefined)
1585
+ throw new Error("invalid steer blocker result");
1586
+ if (phase === "prepare" && mutationBoundary !== "none")
1587
+ throw new Error("preparation crossed mutation boundary");
1588
+ return Object.freeze({
1589
+ ...expected,
1590
+ status: "blocked",
1591
+ blockerCode,
1592
+ observationRequired,
1593
+ mutationBoundary,
1594
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
1595
+ });
1596
+ }
1597
+ if (status === "uncertain") {
1598
+ const quarantine = readOwnData(clone, "quarantine");
1599
+ if (typeof blockerCode !== "string"
1600
+ || observationRequired !== true
1601
+ || mutationBoundary !== "control_may_have_occurred"
1602
+ || (quarantine !== "caller" && quarantine !== "provider")
1603
+ || phase === "prepare") {
1604
+ throw new Error("invalid steer uncertainty");
1605
+ }
1606
+ return Object.freeze({
1607
+ ...expected,
1608
+ status: "uncertain",
1609
+ blockerCode,
1610
+ observationRequired: true,
1611
+ mutationBoundary: "control_may_have_occurred",
1612
+ quarantine,
1613
+ ...(evidenceDigest === undefined ? {} : { evidenceDigest })
1614
+ });
1615
+ }
1616
+ throw new Error("invalid steer phase status");
1617
+ }
1618
+ function assertSteerPreparedIdentity(value, expected) {
1619
+ if (readOwnData(value, "schemaVersion") !== CONTROL_COORDINATOR_SCHEMA_VERSION
1620
+ || readOwnData(value, "parentOperationId") !== expected.parentOperationId
1621
+ || readOwnData(value, "parentRequestDigest") !== expected.parentRequestDigest
1622
+ || readOwnData(value, "parentTargetBindingDigest") !== expected.parentTargetBindingDigest
1623
+ || readOwnData(value, "controlActionId") !== expected.controlActionId
1624
+ || readOwnData(value, "action") !== "steer"
1625
+ || readOwnData(value, "requestDigest") !== expected.requestDigest
1626
+ || readOwnData(value, "expectedAssistantTurnId") !== expected.expectedAssistantTurnId) {
1627
+ throw new Error("steer prepared identity mismatch");
1628
+ }
1629
+ for (const key of ["assistantBranchId", "assistantParentTurnId", "baselineSnapshotDigest", "preparedDigest"]) {
1630
+ const field = readOwnData(value, key);
1631
+ if (typeof field !== "string" || field.length === 0)
1632
+ throw new Error("steer prepared evidence is invalid");
1633
+ }
1634
+ }
1635
+ function assertSteerReceiptIdentity(value, prepared) {
1636
+ if (readOwnData(value, "schemaVersion") !== CONTROL_COORDINATOR_SCHEMA_VERSION
1637
+ || readOwnData(value, "baselineSnapshotDigest") !== prepared.baselineSnapshotDigest
1638
+ || readOwnData(value, "preparedDigest") !== prepared.preparedDigest
1639
+ || readOwnData(value, "assistantTurnId") !== prepared.expectedAssistantTurnId
1640
+ || readOwnData(value, "assistantBranchId") !== prepared.assistantBranchId
1641
+ || readOwnData(value, "assistantParentTurnId") !== prepared.assistantParentTurnId) {
1642
+ throw new Error("steer receipt identity mismatch");
1643
+ }
1644
+ }
1645
+ function phaseFailureCode(signal, deadlineAt, error) {
1646
+ if (signal.aborted)
1647
+ return "operation_cancelled";
1648
+ const code = error !== null && typeof error === "object" && "code" in error
1649
+ ? error.code
1650
+ : undefined;
1651
+ if (code === "deadline_exceeded")
1652
+ return "operation_timeout";
1653
+ if (code === "aborted")
1654
+ return "operation_cancelled";
1655
+ if (Date.now() >= deadlineAt)
1656
+ return "operation_timeout";
1657
+ if (code === "navigation_mismatch" || code === "claim_mismatch" || code === "page_mismatch" || code === "target_binding_mismatch") {
1658
+ return "target_binding_mismatch";
1659
+ }
1660
+ return "target_evidence_unavailable";
1661
+ }
1662
+ /**
1663
+ * Rehydrate a missing binding only through the durable recovery context. The
1664
+ * helper never selects a new page or target and never performs a mutation.
1665
+ */
1666
+ async function steerBindingFor(bindings, ensureRecovered, recovery, operationId, requestDigest, signal) {
1667
+ const binding = bindingFor(bindings, operationId, undefined);
1668
+ if (binding !== undefined || recovery === undefined)
1669
+ return binding;
1670
+ try {
1671
+ await ensureRecovered(operationId, requestDigest, recovery.surface, signal);
1672
+ }
1673
+ catch {
1674
+ return undefined;
1675
+ }
1676
+ return bindingFor(bindings, operationId, undefined);
1677
+ }
1678
+ /**
1679
+ * Unlike a plain `withTabTransaction` call, retain and await the provider
1680
+ * callback when the coordinator rejects its public promise at a deadline.
1681
+ */
1682
+ async function runSteerControlTransaction(binding, operationId, signal, deadlineAt, transactionTimeoutMs, observeCurrentTarget, evidenceDigest, callback) {
1683
+ let workPromise;
1684
+ const transaction = binding.withTabTransaction(boundedRequest(signal, deadlineAt, transactionTimeoutMs, "operation-control-steer-execute", "control"), async (context) => {
1685
+ workPromise = (async () => {
1686
+ const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
1687
+ binding.assertCurrent(current.evidence, current.authoritativeClaim);
1688
+ return await callback(context);
1689
+ })();
1690
+ return await workPromise;
1691
+ });
1692
+ try {
1693
+ return await transaction;
1694
+ }
1695
+ catch (error) {
1696
+ if (workPromise !== undefined)
1697
+ await workPromise.catch(() => undefined);
1698
+ throw error;
1699
+ }
1700
+ }
1701
+ /** Link caller cancellation to a phase-local signal without retaining it. */
1702
+ function createLinkedAbortController(source) {
1703
+ const controller = new AbortController();
1704
+ if (source === undefined)
1705
+ return { controller, cleanup: () => undefined };
1706
+ const onAbort = () => {
1707
+ if (!controller.signal.aborted)
1708
+ controller.abort(source.reason);
1709
+ };
1710
+ if (source.aborted)
1711
+ onAbort();
1712
+ else
1713
+ source.addEventListener("abort", onAbort, { once: true });
1714
+ return {
1715
+ controller,
1716
+ cleanup: () => source.removeEventListener("abort", onAbort)
1717
+ };
1718
+ }
1719
+ /**
1720
+ * Adapt one provider's one-shot Send observers to the four phase protocol.
1721
+ * Precondition reads use the phase's active actor; each postcondition probe
1722
+ * acquires a fresh read actor and releases it before SendOnce sleeps/polls.
1723
+ */
1724
+ function createPhaseSendObservers(binding, operationId, baseObservers, controller, observeCurrentTarget, evidenceDigest, transactionTimeoutMs) {
1725
+ let activeContext;
1726
+ const observers = {
1727
+ observePrecondition: async (request) => {
1728
+ const context = activeContext;
1729
+ const signal = context?.acquisition.signal ?? controller.signal;
1730
+ return await baseObservers.observePrecondition({
1731
+ ...request,
1732
+ ...(context === undefined ? {} : { page: context.page }),
1733
+ signal,
1734
+ ...(context?.acquisition.timing.deadlineAt === undefined
1735
+ ? {}
1736
+ : { deadlineAt: context.acquisition.timing.deadlineAt })
1737
+ });
1738
+ },
1739
+ observePostcondition: async (request) => {
1740
+ const probe = await runSendTransaction(binding, operationId, controller, observeCurrentTarget, evidenceDigest, request.deadlineAt, transactionTimeoutMs, "operation-send-postcondition", "read", context => baseObservers.observePostcondition({
1741
+ ...request,
1742
+ page: context.page,
1743
+ signal: context.acquisition.signal,
1744
+ ...(context.acquisition.timing.deadlineAt === undefined
1745
+ ? {}
1746
+ : { deadlineAt: context.acquisition.timing.deadlineAt })
1747
+ }));
1748
+ if (isPlainPostconditionResult(probe)
1749
+ && probe.status === "blocked"
1750
+ && (probe.blockerCode === "target_evidence_unavailable" || probe.blockerCode === "ambiguous_submit")) {
1751
+ return { result: probe, retryable: true };
1752
+ }
1753
+ return probe;
1754
+ },
1755
+ ...(baseObservers.sleep === undefined ? {} : { sleep: baseObservers.sleep }),
1756
+ ...(baseObservers.maxPostconditionAttempts === undefined ? {} : { maxPostconditionAttempts: baseObservers.maxPostconditionAttempts }),
1757
+ ...(baseObservers.postconditionIntervalMs === undefined ? {} : { postconditionIntervalMs: baseObservers.postconditionIntervalMs }),
1758
+ ...(baseObservers.postconditionTimeoutMs === undefined ? {} : { postconditionTimeoutMs: baseObservers.postconditionTimeoutMs })
1759
+ };
1760
+ return {
1761
+ observers,
1762
+ withContext: async (context, callback) => {
1763
+ activeContext = context;
1764
+ try {
1765
+ return await callback();
1766
+ }
1767
+ finally {
1768
+ if (activeContext === context)
1769
+ activeContext = undefined;
1770
+ }
1771
+ }
1772
+ };
1773
+ }
1774
+ /**
1775
+ * Run a single target revalidation plus provider callback in one coordinator
1776
+ * transaction. The coordinator may reject its public promise at a deadline
1777
+ * while its callback is still in flight; retain the callback promise and await
1778
+ * it before propagating that error so an activation can never be overlapped.
1779
+ */
1780
+ async function runSendTransaction(binding, operationId, controller, observeCurrentTarget, evidenceDigest, requestedDeadlineAt, transactionTimeoutMs, label, priority, callback) {
1781
+ let workPromise;
1782
+ const transaction = binding.withTabTransaction(boundedRequest(controller.signal, requestedDeadlineAt, transactionTimeoutMs, label, priority), async (context) => {
1783
+ const abortFromCoordinator = () => {
1784
+ if (!controller.signal.aborted)
1785
+ controller.abort(context.acquisition.signal.reason);
1786
+ };
1787
+ if (context.acquisition.signal.aborted)
1788
+ abortFromCoordinator();
1789
+ else
1790
+ context.acquisition.signal.addEventListener("abort", abortFromCoordinator, { once: true });
1791
+ workPromise = (async () => {
1792
+ const current = await readCurrentTarget(binding, operationId, context.page, observeCurrentTarget, evidenceDigest, context.acquisition.signal, context.acquisition.timing.deadlineAt);
1793
+ binding.assertCurrent(current.evidence, current.authoritativeClaim);
1794
+ return await callback(context);
1795
+ })();
1796
+ try {
1797
+ return await workPromise;
1798
+ }
1799
+ finally {
1800
+ context.acquisition.signal.removeEventListener("abort", abortFromCoordinator);
1801
+ }
1802
+ });
1803
+ try {
1804
+ return await transaction;
1805
+ }
1806
+ catch (error) {
1807
+ // `workPromise` is set before target proof/provider work starts. Awaiting it
1808
+ // here preserves coordinator quarantine semantics without releasing an
1809
+ // actor while the browser call is still running.
1810
+ if (workPromise !== undefined)
1811
+ await workPromise.catch(() => undefined);
1812
+ throw error;
1813
+ }
1814
+ }
1815
+ function blockedPrepareSend(blockerCode) {
1816
+ return { status: "blocked", result: { status: "blocked", blockerCode } };
1817
+ }
1818
+ function resultToPrepareSend(result) {
1819
+ return { status: "blocked", result };
1820
+ }
1821
+ function blockedExecuteSend(blockerCode) {
1822
+ return { status: "blocked", result: { status: "blocked", blockerCode } };
1823
+ }
1824
+ function phaseBlocker(signal, deadlineAt) {
1825
+ if (signal.aborted)
1826
+ return "operation_cancelled";
1827
+ if (deadlineAt !== undefined && Date.now() >= deadlineAt)
1828
+ return "operation_timeout";
1829
+ return "port_protocol_violation";
1830
+ }
1831
+ function submissionPreparedFromSendOnce(prepared, expected) {
1832
+ const baseline = prepared.baseline.ownershipBaseline;
1833
+ if (baseline === undefined)
1834
+ throw new Error("missing durable Send baseline");
1835
+ return normalizeSubmissionPreparedSend({
1836
+ prepared: cloneFrozenData(prepared),
1837
+ baseline: cloneFrozenData(baseline),
1838
+ evidenceDigest: prepared.observation.evidenceDigest
1839
+ }, expected, {
1840
+ operationId: prepared.operationId,
1841
+ requestDigest: prepared.requestDigest,
1842
+ surface: prepared.surface,
1843
+ actionId: prepared.actionId
1844
+ });
1845
+ }
1846
+ function normalizeSubmissionPreparedSend(value, expected, identity) {
1847
+ const clone = cloneFrozenData(value);
1848
+ if (!isPlainDataRecord(clone))
1849
+ throw new Error("invalid opaque Send capability");
1850
+ assertExactDataKeys(clone, ["prepared", "baseline", "evidenceDigest"]);
1851
+ const prepared = readOwnData(clone, "prepared");
1852
+ const baseline = readOwnData(clone, "baseline");
1853
+ const evidence = readOwnData(clone, "evidenceDigest");
1854
+ if (!isPlainDataRecord(prepared) || !isPlainDataRecord(baseline) || typeof evidence !== "string" || !DIGEST_PATTERN.test(evidence)) {
1855
+ throw new Error("invalid opaque Send capability");
1856
+ }
1857
+ assertExactDataKeys(prepared, ["schemaVersion", "operationId", "requestDigest", "surface", "actionId", "expected", "observation", "baseline"]);
1858
+ if (readOwnData(prepared, "schemaVersion") !== "chatgpt.browser_control.send_once_prepared.v1"
1859
+ || readOwnData(prepared, "operationId") !== identity.operationId
1860
+ || readOwnData(prepared, "requestDigest") !== identity.requestDigest
1861
+ || readOwnData(prepared, "surface") !== identity.surface
1862
+ || readOwnData(prepared, "actionId") !== identity.actionId
1863
+ || canonicalJson(readOwnData(prepared, "expected")) !== canonicalJson(expected)) {
1864
+ throw new Error("opaque Send identity mismatch");
1865
+ }
1866
+ const preparedBaseline = readOwnData(prepared, "baseline");
1867
+ if (!isPlainDataRecord(preparedBaseline)) {
1868
+ throw new Error("missing opaque Send baseline");
1869
+ }
1870
+ const ownershipBaseline = readOwnData(preparedBaseline, "ownershipBaseline");
1871
+ if (!isPlainDataRecord(ownershipBaseline) || canonicalJson(ownershipBaseline) !== canonicalJson(baseline)) {
1872
+ throw new Error("opaque Send baseline mismatch");
1873
+ }
1874
+ const observation = readOwnData(prepared, "observation");
1875
+ if (!isPlainDataRecord(observation) || readOwnData(observation, "status") !== "exact" || readOwnData(observation, "evidenceDigest") !== evidence) {
1876
+ throw new Error("opaque Send observation mismatch");
1877
+ }
1878
+ return clone;
1879
+ }
1880
+ function assertExactDataKeys(value, keys) {
1881
+ const allowed = new Set(keys);
1882
+ for (const key of Object.keys(value)) {
1883
+ if (!allowed.has(key))
1884
+ throw new Error("unsupported opaque Send field");
1885
+ }
1886
+ }
1887
+ function markEstablished(binding, result) {
1888
+ if (binding.markTargetEstablished !== undefined
1889
+ && (result.status === "submitted" || result.status === "already_submitted")
1890
+ && result.targetEstablishment !== undefined) {
1891
+ binding.markTargetEstablished({
1892
+ conversationId: result.targetEstablishment.conversationId,
1893
+ canonicalThreadUrl: result.targetEstablishment.canonicalThreadUrl
1894
+ });
1895
+ }
1896
+ }
1897
+ async function readCurrentTarget(binding, operationId, page, observeCurrentTarget, evidenceDigest, signal = new AbortController().signal, deadlineAt) {
1898
+ if (observeCurrentTarget !== undefined) {
1899
+ const observed = await observeCurrentTarget({ page, operationId, target: binding.target, signal, ...(deadlineAt === undefined ? {} : { deadlineAt }) });
1900
+ return normalizeCurrentTargetResult(observed);
1901
+ }
1902
+ // The default read is a bounded, read-only browser observation. Provider
1903
+ // implementations can inject a narrower identity probe when their DOM
1904
+ // exposes stable target evidence without a complete turn snapshot.
1905
+ const target = binding.target;
1906
+ const boundThreadId = binding.evidence.thread.status === "available"
1907
+ ? binding.evidence.thread.value
1908
+ : undefined;
1909
+ const boundClaim = binding.evidence.authoritativeTabClaim.status === "available"
1910
+ ? binding.evidence.authoritativeTabClaim.value
1911
+ : undefined;
1912
+ const result = await observeBrowserPage(page, {
1913
+ operationId,
1914
+ target: {
1915
+ providerId: target.providerId,
1916
+ browserId: target.browserId,
1917
+ tabId: target.tabId,
1918
+ coordinationScope: target.coordinationScope,
1919
+ ...(boundClaim === undefined ? {} : { authoritativeTabClaim: boundClaim }),
1920
+ ...(target.targetLifecycle === undefined ? {} : { targetLifecycle: target.targetLifecycle }),
1921
+ ...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId }),
1922
+ ...(boundThreadId === undefined ? {} : { expectedThreadId: boundThreadId })
1923
+ },
1924
+ // An injected digest is required for target evidence. Never manufacture a
1925
+ // placeholder digest: unavailable evidence must remain unavailable.
1926
+ evidenceDigest: evidenceDigest ?? (() => {
1927
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1928
+ }),
1929
+ responseContent: "metadata"
1930
+ });
1931
+ return { evidence: result.snapshot.target };
1932
+ }
1933
+ function normalizeCurrentTargetResult(value) {
1934
+ if (!isPlainDataRecord(value))
1935
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1936
+ const evidence = readOwnData(value, "evidence");
1937
+ if (evidence === undefined)
1938
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1939
+ const authoritativeClaim = readOwnData(value, "authoritativeClaim");
1940
+ if (!isSafeDataGraph(evidence) || (authoritativeClaim !== undefined && !isSafeDataGraph(authoritativeClaim))) {
1941
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1942
+ }
1943
+ try {
1944
+ for (const key of Object.keys(value)) {
1945
+ if (key !== "evidence" && key !== "authoritativeClaim") {
1946
+ throw new Error("unsupported current target field");
1947
+ }
1948
+ }
1949
+ }
1950
+ catch {
1951
+ throw new OperationBrowserAdapterError("target_evidence_unavailable");
1952
+ }
1953
+ return Object.freeze({
1954
+ evidence: evidence,
1955
+ ...(authoritativeClaim === undefined ? {} : { authoritativeClaim: authoritativeClaim })
1956
+ });
1957
+ }
1958
+ function boundedRequest(signal, requestedDeadlineAt, transactionTimeoutMs, label, priority = "read") {
1959
+ const localDeadline = Date.now() + transactionTimeoutMs;
1960
+ return {
1961
+ priority,
1962
+ signal: signal ?? new AbortController().signal,
1963
+ deadlineAt: Math.min(requestedDeadlineAt ?? MAX_DEADLINE_AT, localDeadline),
1964
+ label
1965
+ };
1966
+ }
1967
+ async function defaultCollectorObservation(request, transaction, context, evidenceDigest, authoritativeClaim) {
1968
+ const target = transaction.target;
1969
+ const observation = await observeBrowserPage(transaction.page, {
1970
+ operationId: request.operationId,
1971
+ target: {
1972
+ providerId: target.providerId,
1973
+ browserId: target.browserId,
1974
+ tabId: target.tabId,
1975
+ coordinationScope: target.coordinationScope,
1976
+ ...(authoritativeClaim === undefined ? {} : { authoritativeTabClaim: authoritativeClaim.token }),
1977
+ ...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId })
1978
+ },
1979
+ evidenceDigest,
1980
+ responseContent: request.responseContent,
1981
+ ...(request.responseFormat === undefined ? {} : { responseFormat: request.responseFormat }),
1982
+ ...(context.baseline === undefined ? {} : { baseline: context.baseline }),
1983
+ ...(context.prior?.assistantTurnId === undefined ? {} : {
1984
+ terminalAssistantTurnId: context.prior.assistantTurnId,
1985
+ ...(request.responseContent === "include" ? { rawAssistantTurnId: context.prior.assistantTurnId } : {})
1986
+ })
1987
+ });
1988
+ return {
1989
+ schemaVersion: "chatgpt.browser_control.collector.v1",
1990
+ snapshot: observation.snapshot,
1991
+ ...(observation.terminal === undefined ? {} : { terminal: observation.terminal })
1992
+ };
1993
+ }
1994
+ function sleepOutsideCoordinator(milliseconds, signal) {
1995
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 60_000) {
1996
+ return Promise.reject(new OperationBrowserAdapterError("adapter_incomplete"));
1997
+ }
1998
+ if (signal.aborted)
1999
+ return Promise.reject(new OperationBrowserAdapterError("browser_bridge_unavailable"));
2000
+ return new Promise((resolve, reject) => {
2001
+ const timer = setTimeout(() => {
2002
+ signal.removeEventListener("abort", onAbort);
2003
+ resolve();
2004
+ }, milliseconds);
2005
+ const onAbort = () => {
2006
+ clearTimeout(timer);
2007
+ signal.removeEventListener("abort", onAbort);
2008
+ reject(new OperationBrowserAdapterError("browser_bridge_unavailable"));
2009
+ };
2010
+ signal.addEventListener("abort", onAbort, { once: true });
2011
+ });
2012
+ }
2013
+ function isPlainPostconditionResult(value) {
2014
+ if (value === null || typeof value !== "object" || Array.isArray(value))
2015
+ return false;
2016
+ const descriptor = Object.getOwnPropertyDescriptor(value, "status");
2017
+ return descriptor !== undefined && "value" in descriptor && typeof descriptor.value === "string";
2018
+ }
2019
+ function errorCode(error) {
2020
+ if (error instanceof OperationBrowserAdapterError)
2021
+ return error.code;
2022
+ return "target_evidence_unavailable";
2023
+ }