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

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