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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. package/references/streaming.md +6 -0
@@ -0,0 +1,1073 @@
1
+ import { composerTextbox, stopGenerationButton } from "../dom/selectors.js";
2
+ import { observeBrowserPage } from "./browser-observation.js";
3
+ import { COLLECTOR_SCHEMA_VERSION } from "./collector.js";
4
+ import { classifyTurnOwnership, TURN_OWNERSHIP_SCHEMA_VERSION, } from "./turn-ownership.js";
5
+ /**
6
+ * A small, provider-facing primitive layer for the transactional adapter.
7
+ *
8
+ * This module intentionally does not call a legacy command. Each callback
9
+ * performs a bounded locator/evaluation operation, or one browser activation,
10
+ * and returns only keyed evidence. Attachment handoff, configuration,
11
+ * The base factory deliberately leaves configuration, attachments, and Work
12
+ * steer to their dedicated provider modules. `chatgpt-runtime.ts` composes
13
+ * those modules into the default ChatGPT operation runtime; the inventory
14
+ * below describes this base factory, not the completed composite runtime.
15
+ */
16
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
17
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
18
+ const OPAQUE_THREAD_URL_PATTERN = /^https:\/\/opaque\.invalid\/thread\/[0-9a-f]{64}$/u;
19
+ const MAX_LOCATOR_CANDIDATES = 128;
20
+ const MAX_COMPOSER_CHARS = 8 * 1024 * 1024;
21
+ export const PRODUCTION_PRIMITIVE_CAPABILITIES = Object.freeze([
22
+ "composer_set",
23
+ "empty_attachment_observation",
24
+ "send_activation",
25
+ "collector_snapshot",
26
+ "durable_baseline_projection",
27
+ "submission_witness_recovery",
28
+ "stop_control"
29
+ ]);
30
+ /**
31
+ * These are not soft feature flags. They are an inventory of deliberately
32
+ * missing proof, so a caller cannot mistake an unavailable primitive for a
33
+ * best-effort browser fallback.
34
+ */
35
+ export const UNWIRED_PRODUCTION_PRIMITIVES = Object.freeze([
36
+ "configuration_set",
37
+ "tool_selection",
38
+ "power_select",
39
+ "file_chooser_handoff",
40
+ "attachment_identity_for_nonempty_manifest",
41
+ "work_steer_activation"
42
+ ]);
43
+ export const PRODUCTION_OPERATION_PRIMITIVE_INVENTORY = Object.freeze({
44
+ scope: "base_primitive_factory",
45
+ wired: PRODUCTION_PRIMITIVE_CAPABILITIES,
46
+ unwired: UNWIRED_PRODUCTION_PRIMITIVES
47
+ });
48
+ export class ProductionPrimitiveError extends Error {
49
+ code;
50
+ constructor(code) {
51
+ super("The provider-specific operation primitive could not prove the requested action safely.");
52
+ this.code = code;
53
+ this.name = "ProductionPrimitiveError";
54
+ }
55
+ }
56
+ /**
57
+ * Create one request-scoped set of production operation primitives.
58
+ *
59
+ * `operationId`, `requestDigest`, and the composer value should normally be
60
+ * supplied by the lazy runtime capture after the journal has created the
61
+ * operation. If either identity is absent, Send fails closed rather than
62
+ * fabricating an evidence domain.
63
+ */
64
+ export function createProductionOperationPrimitives(options) {
65
+ validateOptions(options);
66
+ const state = {
67
+ ...(options.operationId === undefined ? {} : { operationId: options.operationId }),
68
+ ...(options.requestDigest === undefined ? {} : { requestDigest: options.requestDigest }),
69
+ ...(options.desiredComposerText !== undefined
70
+ ? { desiredComposerText: options.desiredComposerText }
71
+ : options.composerText === undefined ? {} : { desiredComposerText: options.composerText }),
72
+ ...(options.authoritativeTabClaim === undefined ? {} : { authoritativeTabClaim: options.authoritativeTabClaim }),
73
+ ...(options.target === undefined ? {} : { target: options.target })
74
+ };
75
+ const staging = Object.freeze({
76
+ readCurrent: request => readStaging(request, options.evidenceDigest, state),
77
+ mutateOnce: request => mutateStagingOnce(request, options.evidenceDigest, state),
78
+ observe: request => readStaging(request, options.evidenceDigest, state)
79
+ });
80
+ const sendObservers = Object.freeze({
81
+ observePrecondition: request => observeSendPrecondition(request, options.evidenceDigest, state, options.observeAttachments),
82
+ observePostcondition: async (request) => {
83
+ const result = await observeSendPostcondition(request, options.evidenceDigest, state);
84
+ return result.status === "blocked"
85
+ && (result.blockerCode === "ambiguous_submit" || result.blockerCode === "target_evidence_unavailable")
86
+ ? { result, retryable: true }
87
+ : result;
88
+ },
89
+ // Every retry is an observation-only transaction. The delay remains
90
+ // outside the tab actor and can never repeat the Send activation.
91
+ sleep: sleepOutsideBrowser,
92
+ // ChatGPT can establish the canonical conversation URL before it exposes
93
+ // stable user/assistant DOM identities. Keep that provider settling window
94
+ // bounded without collapsing a successful one-shot Send into uncertainty.
95
+ maxPostconditionAttempts: 32,
96
+ postconditionIntervalMs: 250,
97
+ postconditionTimeoutMs: 15_000
98
+ });
99
+ const submission = Object.freeze({
100
+ observeStaging: (request, page, target) => observeSubmissionStaging(request, page, target, options.evidenceDigest, state),
101
+ // The file identity layer revalidates paths outside the actor. There is
102
+ // still no identity-grade provider chooser primitive here, so never begin
103
+ // a chooser mutation under an invented selector or wait loop.
104
+ handoffFiles: async () => ({
105
+ status: "not_satisfied",
106
+ blockerCode: "attachment_manifest_mismatch"
107
+ }),
108
+ observeAttachments: (request, page, target) => observeSubmissionAttachments(request, page, target, options.evidenceDigest, state, options.observeAttachments),
109
+ sendObservers
110
+ });
111
+ const collector = Object.freeze({
112
+ readContext: (request, page, target) => readCollectorContext(request, page, target, options.evidenceDigest, state),
113
+ observe: (request, page, target, context) => observeCollector(request, page, target, context, options.evidenceDigest, state),
114
+ // This timer is intentionally outside any tab transaction. The browser
115
+ // adapter invokes it after its short observation transaction has settled.
116
+ sleep: sleepOutsideBrowser
117
+ });
118
+ const control = Object.freeze({
119
+ observeTurn: (request, page, target) => observeControlTurn(request, page, target, options.evidenceDigest, state),
120
+ executeOnce: (request, page, target) => executeControlOnce(request, page, target, options.evidenceDigest, state),
121
+ observePostcondition: (request, page, target) => observeControlPostcondition(request, page, target, options.evidenceDigest, state)
122
+ });
123
+ return Object.freeze({
124
+ staging,
125
+ submission,
126
+ collector,
127
+ control
128
+ });
129
+ }
130
+ /** Descriptive aliases for integrations that name the layer after the adapter. */
131
+ export const createOperationProductionPrimitives = createProductionOperationPrimitives;
132
+ export const createProductionPrimitives = createProductionOperationPrimitives;
133
+ async function readStaging(request, evidenceDigest, state) {
134
+ if (request.kind !== "composer_set") {
135
+ return unavailableStaging(request, stagingUnwiredCode(request.kind));
136
+ }
137
+ if (state.desiredComposerText === undefined) {
138
+ return unavailableStaging(request, "composer_primitive_unwired");
139
+ }
140
+ if (!matchesExpectedStagingDigest(request, evidenceDigest)) {
141
+ return unavailableStaging(request, "composer_request_mismatch");
142
+ }
143
+ const current = await readComposerState(request.page, request.target, request.operationId, evidenceDigest);
144
+ if (current === undefined)
145
+ return unavailableStaging(request, "composer_control_unavailable");
146
+ const satisfied = current.text === state.desiredComposerText;
147
+ const evidence = digest(evidenceDigest, "composer-observation", {
148
+ operationId: request.operationId,
149
+ targetBindingDigest: request.targetBindingDigest,
150
+ currentStateDigest: current.currentStateDigest,
151
+ status: satisfied ? "satisfied" : "not_satisfied"
152
+ });
153
+ if (evidence === undefined)
154
+ return unavailableStaging(request, "evidence_digest_failed");
155
+ return {
156
+ status: satisfied ? "satisfied" : "not_satisfied",
157
+ desiredStateDigest: request.desiredStateDigest,
158
+ currentStateDigest: current.currentStateDigest,
159
+ evidenceDigest: evidence
160
+ };
161
+ }
162
+ async function mutateStagingOnce(request, evidenceDigest, state) {
163
+ if (request.kind !== "composer_set" || state.desiredComposerText === undefined) {
164
+ throw new ProductionPrimitiveError(request.kind === "composer_set" ? "composer_primitive_unwired" : stagingUnwiredCode(request.kind));
165
+ }
166
+ if (!isDigest(request.desiredStateDigest) || !isDigest(request.requestDigest)) {
167
+ throw new ProductionPrimitiveError("composer_request_mismatch");
168
+ }
169
+ const expected = safeDigestWith(evidenceDigest, "staging-desired", { requestDigest: request.requestDigest, kind: request.kind });
170
+ if (expected === undefined || expected !== request.desiredStateDigest) {
171
+ throw new ProductionPrimitiveError("composer_request_mismatch");
172
+ }
173
+ const locator = await uniqueVisibleLocator(request.page, composerTextbox);
174
+ if (locator === undefined || typeof locator.fill !== "function") {
175
+ throw new ProductionPrimitiveError("composer_control_unavailable");
176
+ }
177
+ // The sole reversible composer mutation. There is no readiness wait and no
178
+ // fallback press/click path if this call rejects.
179
+ await locator.fill(state.desiredComposerText);
180
+ return { status: "started" };
181
+ }
182
+ function stagingUnwiredCode(kind) {
183
+ switch (kind) {
184
+ case "configuration_set": return "configuration_primitive_unwired";
185
+ case "tool_set": return "tool_primitive_unwired";
186
+ case "power_select": return "power_primitive_unwired";
187
+ case "composer_set": return "composer_primitive_unwired";
188
+ }
189
+ }
190
+ function unavailableStaging(request, blockerCode) {
191
+ return {
192
+ status: "unavailable",
193
+ desiredStateDigest: request.desiredStateDigest,
194
+ blockerCode
195
+ };
196
+ }
197
+ function matchesExpectedStagingDigest(request, evidenceDigest) {
198
+ return safeDigestWith(evidenceDigest, "staging-desired", {
199
+ requestDigest: request.requestDigest,
200
+ kind: request.kind
201
+ }) === request.desiredStateDigest;
202
+ }
203
+ async function observeSubmissionStaging(request, page, target, evidenceDigest, state) {
204
+ state.target = target;
205
+ if (!isDigest(request.configurationReceiptDigest) || !isDigest(request.composerReceiptDigest)) {
206
+ return { status: "unavailable", reason: "unknown" };
207
+ }
208
+ const expectedConfiguration = state.requestDigest === undefined
209
+ ? undefined
210
+ : safeDigestWith(evidenceDigest, "configuration-request", state.requestDigest);
211
+ const expectedComposer = state.requestDigest === undefined
212
+ ? undefined
213
+ : safeDigestWith(evidenceDigest, "composer-request", state.requestDigest);
214
+ if (expectedConfiguration === undefined || expectedComposer === undefined) {
215
+ return { status: "unavailable", reason: "target" };
216
+ }
217
+ if (request.configurationReceiptDigest !== expectedConfiguration) {
218
+ const evidence = safeDigestWith(evidenceDigest, "submission-stage", { operationId: request.operationId, reason: "configuration" });
219
+ return evidence === undefined
220
+ ? { status: "mismatch", reason: "configuration" }
221
+ : { status: "mismatch", reason: "configuration", evidenceDigest: evidence };
222
+ }
223
+ if (request.composerReceiptDigest !== expectedComposer || state.desiredComposerText === undefined) {
224
+ return { status: "unavailable", reason: "composer" };
225
+ }
226
+ const current = await readComposerState(page, target, request.operationId, evidenceDigest);
227
+ if (current === undefined)
228
+ return { status: "unavailable", reason: "composer" };
229
+ if (current.text !== state.desiredComposerText) {
230
+ return { status: "mismatch", reason: "composer", evidenceDigest: current.evidenceDigest };
231
+ }
232
+ return {
233
+ status: "exact",
234
+ evidenceDigest: current.evidenceDigest
235
+ };
236
+ }
237
+ async function observeSendPrecondition(request, evidenceDigest, state, attachmentObserver) {
238
+ const identity = sendIdentity(state, request.expected);
239
+ if (identity === undefined)
240
+ return { status: "unavailable", code: "target_evidence_unavailable" };
241
+ const target = targetForObservation(request.expected, state);
242
+ if (target === undefined)
243
+ return { status: "unavailable", code: "target_evidence_unavailable" };
244
+ // Recovery after a durable Send intent is observation-only. A pending new
245
+ // target necessarily no longer renders the blank surface, so re-running the
246
+ // normal precondition would reject the very post-Send state we must inspect.
247
+ // The durable blank-task snapshot is the bounded baseline authority here;
248
+ // the subsequent postcondition still has to prove exactly one new user turn
249
+ // and complete provider conversation identity before establishment.
250
+ if (request.mode === "observe_only" && state.target?.targetLifecycle === "new_pending") {
251
+ const recoveryBaseline = recoverPendingBlankBaseline(state);
252
+ if (recoveryBaseline === undefined)
253
+ return { status: "unavailable", code: "target_evidence_unavailable" };
254
+ return {
255
+ status: "exact",
256
+ targetBindingDigest: request.expected.targetBindingDigest,
257
+ configurationReceiptDigest: request.expected.configurationReceiptDigest,
258
+ composerReceiptDigest: request.expected.composerReceiptDigest,
259
+ attachments: {
260
+ count: request.expected.attachmentManifest.count,
261
+ orderPolicy: "exact",
262
+ identityDigests: request.expected.attachmentManifest.identities.map(entry => entry.identityDigest)
263
+ },
264
+ baseline: {
265
+ ownershipBaseline: recoveryBaseline,
266
+ userTurnEvidenceDigest: safeDigestWith(evidenceDigest, "send-baseline", {
267
+ snapshotDigest: recoveryBaseline.snapshotDigest,
268
+ userTurns: []
269
+ }) ?? recoveryBaseline.snapshotDigest
270
+ },
271
+ evidenceDigest: recoveryBaseline.snapshotDigest
272
+ };
273
+ }
274
+ if (state.desiredComposerText === undefined)
275
+ return { status: "unavailable", code: "composer_drift" };
276
+ const expectedComposer = safeDigestWith(evidenceDigest, "composer-request", state.requestDigest);
277
+ const expectedConfiguration = safeDigestWith(evidenceDigest, "configuration-request", state.requestDigest);
278
+ if (expectedComposer === undefined || expectedConfiguration === undefined) {
279
+ return { status: "unavailable", code: "target_evidence_unavailable" };
280
+ }
281
+ if (request.expected.composerReceiptDigest !== expectedComposer) {
282
+ return { status: "mismatch", code: "composer_drift" };
283
+ }
284
+ if (request.expected.configurationReceiptDigest !== expectedConfiguration) {
285
+ return { status: "mismatch", code: "configuration_drift" };
286
+ }
287
+ let observation;
288
+ try {
289
+ observation = await observeBrowserPage(request.page, {
290
+ operationId: identity,
291
+ target,
292
+ evidenceDigest,
293
+ responseContent: "metadata"
294
+ });
295
+ }
296
+ catch {
297
+ return { status: "unavailable", code: "target_evidence_unavailable" };
298
+ }
299
+ const snapshot = observation.snapshot;
300
+ if (state.target?.targetLifecycle === "new_pending") {
301
+ const anchor = observation.newTargetAnchor;
302
+ if (anchor === undefined
303
+ || anchor.anchorDigest !== state.target.newTargetAnchorDigest
304
+ || anchor.blankTaskEvidenceDigest !== state.target.blankTaskEvidenceDigest
305
+ || snapshot.target.thread.status !== "unavailable"
306
+ || snapshot.target.conversation.status !== "unavailable"
307
+ || snapshot.target.canonicalThreadUrl.status !== "unavailable"
308
+ || snapshot.userTurns.length !== 0
309
+ || snapshot.assistantTurns.length !== 0) {
310
+ return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
311
+ }
312
+ }
313
+ if (!stableBaseline(snapshot)) {
314
+ return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
315
+ }
316
+ const composerTarget = state.target;
317
+ if (composerTarget === undefined)
318
+ return { status: "unavailable", code: "target_evidence_unavailable" };
319
+ const composer = await readComposerState(request.page, composerTarget, identity, evidenceDigest);
320
+ if (composer === undefined)
321
+ return { status: "unavailable", code: "composer_drift", evidenceDigest: snapshot.snapshotDigest };
322
+ if (composer.text !== state.desiredComposerText) {
323
+ return { status: "mismatch", code: "composer_drift", evidenceDigest: composer.evidenceDigest };
324
+ }
325
+ const attachments = await observeAttachmentEnvelope({
326
+ operationId: identity,
327
+ requestDigest: state.requestDigest,
328
+ surface: request.expected.surface,
329
+ targetBindingDigest: request.expected.targetBindingDigest,
330
+ manifest: request.expected.attachmentManifest
331
+ }, request.page, composerTarget, evidenceDigest, state, attachmentObserver);
332
+ if (attachments.status !== "absent" && attachments.status !== "exact") {
333
+ return { status: "unavailable", code: "attachment_manifest_mismatch", evidenceDigest: snapshot.snapshotDigest };
334
+ }
335
+ const baseline = baselineForSnapshot(snapshot, evidenceDigest, identity, request.expected.targetBindingDigest);
336
+ if (baseline === undefined)
337
+ return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
338
+ const sendAttachments = {
339
+ count: attachments.count,
340
+ orderPolicy: "exact",
341
+ identityDigests: [...attachments.identityDigests]
342
+ };
343
+ return {
344
+ status: "exact",
345
+ targetBindingDigest: request.expected.targetBindingDigest,
346
+ configurationReceiptDigest: request.expected.configurationReceiptDigest,
347
+ composerReceiptDigest: request.expected.composerReceiptDigest,
348
+ attachments: sendAttachments,
349
+ baseline: {
350
+ ...(baseline.userTurns.at(-1)?.stableId === undefined ? {} : { userTurnId: baseline.userTurns.at(-1).stableId }),
351
+ ownershipBaseline: baseline,
352
+ userTurnEvidenceDigest: safeDigestWith(evidenceDigest, "send-baseline", {
353
+ snapshotDigest: baseline.snapshotDigest,
354
+ userTurns: baseline.userTurns.map(turn => turn.evidenceDigest)
355
+ }) ?? baseline.snapshotDigest
356
+ },
357
+ evidenceDigest: snapshot.snapshotDigest
358
+ };
359
+ }
360
+ async function observeSendPostcondition(request, evidenceDigest, state) {
361
+ const identity = sendIdentity(state, request.expected);
362
+ if (identity === undefined)
363
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
364
+ // The SendOnce coordinator carries the complete baseline that was either
365
+ // durably appended before activation or projected by the service after a
366
+ // restart. Never fall back to the request-scoped map/current page here.
367
+ const baseline = request.baseline.ownershipBaseline;
368
+ if (baseline === undefined)
369
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
370
+ // A process restart loses the in-memory baseline map. The durable pending
371
+ // target's blank-task evidence is the only safe recovery prefix: it proves
372
+ // that the operation owned zero turns before its one durable Send intent.
373
+ const target = targetForObservation(request.expected, state);
374
+ if (target === undefined)
375
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
376
+ let observation;
377
+ try {
378
+ observation = await observeBrowserPage(request.page, {
379
+ operationId: identity,
380
+ target,
381
+ evidenceDigest,
382
+ responseContent: "metadata",
383
+ baseline
384
+ });
385
+ }
386
+ catch {
387
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
388
+ }
389
+ const delta = observation.snapshot.postSendDelta;
390
+ if (delta === undefined || delta.baselineSnapshotDigest !== baseline.snapshotDigest) {
391
+ return { status: "blocked", blockerCode: "ambiguous_submit", evidenceDigest: observation.snapshot.snapshotDigest };
392
+ }
393
+ const added = observation.snapshot.userTurns.filter(turn => delta.addedUserEvidenceDigests.includes(turn.evidenceDigest));
394
+ if (added.length !== 1) {
395
+ return {
396
+ status: "blocked",
397
+ blockerCode: added.length > 1 ? "concurrent_user_turn" : "ambiguous_submit",
398
+ evidenceDigest: observation.snapshot.snapshotDigest
399
+ };
400
+ }
401
+ const user = added[0];
402
+ if (user?.stableId === undefined) {
403
+ return { status: "blocked", blockerCode: "target_evidence_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
404
+ }
405
+ const assistant = observation.snapshot.assistantTurns.find(turn => turn.parentStableId === user.stableId);
406
+ const status = request.mode === "observe_only" ? "already_submitted" : "submitted";
407
+ const established = state.target?.targetLifecycle === "new_pending"
408
+ ? (() => {
409
+ const conversation = observation.snapshot.target.conversation;
410
+ const canonicalThreadUrl = observation.snapshot.target.canonicalThreadUrl;
411
+ if (conversation.status !== "available"
412
+ || canonicalThreadUrl.status !== "available"
413
+ || state.target.newTargetAnchorDigest === undefined)
414
+ return undefined;
415
+ return {
416
+ targetBindingDigest: request.expected.targetBindingDigest,
417
+ anchorDigest: state.target.newTargetAnchorDigest,
418
+ causalSendActionId: request.actionId,
419
+ conversationId: conversation.value,
420
+ canonicalThreadUrl: canonicalThreadUrl.value,
421
+ userTurnId: user.stableId,
422
+ userTurnEvidenceDigest: user.evidenceDigest,
423
+ postSendDeltaDigest: delta.deltaDigest,
424
+ evidenceDigest: observation.snapshot.snapshotDigest
425
+ };
426
+ })()
427
+ : undefined;
428
+ if (state.target?.targetLifecycle === "new_pending" && established === undefined) {
429
+ return { status: "blocked", blockerCode: "target_evidence_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
430
+ }
431
+ return {
432
+ status,
433
+ targetBindingDigest: request.expected.targetBindingDigest,
434
+ evidenceDigest: observation.snapshot.snapshotDigest,
435
+ userTurnId: user.stableId,
436
+ userTurnEvidenceDigest: user.evidenceDigest,
437
+ postSendDeltaDigest: delta.deltaDigest,
438
+ ...(assistant?.stableId === undefined ? {} : { assistantTurnId: assistant.stableId }),
439
+ ...(established === undefined ? {} : { targetEstablishment: established })
440
+ };
441
+ }
442
+ async function observeSubmissionAttachments(request, page, target, evidenceDigest, state, attachmentObserver) {
443
+ state.target = target;
444
+ return await observeAttachmentEnvelope(request, page, target, evidenceDigest, state, attachmentObserver);
445
+ }
446
+ async function observeAttachmentEnvelope(request, page, target, evidenceDigest, state, attachmentObserver) {
447
+ if (attachmentObserver !== undefined && request.manifest.count > 0) {
448
+ try {
449
+ return await attachmentObserver(request, page, target);
450
+ }
451
+ catch {
452
+ return { status: "unavailable" };
453
+ }
454
+ }
455
+ if (request.manifest.count > 0)
456
+ return { status: "unavailable" };
457
+ const result = await readEmptyAttachmentState(page);
458
+ if (result === undefined || !result.supported)
459
+ return { status: "unavailable" };
460
+ if (result.count !== 0 || result.visibleAttachmentCount !== 0)
461
+ return { status: "mismatch" };
462
+ const evidence = safeDigestWith(evidenceDigest, "composer-attachments", {
463
+ operationId: request.operationId,
464
+ targetBindingDigest: request.targetBindingDigest,
465
+ count: 0
466
+ });
467
+ if (evidence === undefined)
468
+ return { status: "unavailable" };
469
+ return {
470
+ status: "absent",
471
+ evidenceDigest: evidence,
472
+ count: 0,
473
+ orderPolicy: "exact",
474
+ identityDigests: []
475
+ };
476
+ }
477
+ async function readEmptyAttachmentState(page) {
478
+ if (typeof page.evaluate !== "function")
479
+ return undefined;
480
+ try {
481
+ const result = await page.evaluate(() => {
482
+ const visible = (element) => {
483
+ let ancestor = element;
484
+ for (let depth = 0; ancestor !== null && depth < 4096; depth += 1) {
485
+ if (ancestor.nodeType === 1) {
486
+ const candidate = ancestor;
487
+ if (candidate.hasAttribute("hidden") || candidate.hasAttribute("inert") || candidate.getAttribute("aria-hidden") === "true")
488
+ return false;
489
+ }
490
+ ancestor = ancestor.parentNode;
491
+ }
492
+ if (ancestor !== null)
493
+ throw new Error("node limit exceeded");
494
+ const style = window.getComputedStyle(element);
495
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0")
496
+ return false;
497
+ const rect = typeof element.getBoundingClientRect === "function" ? element.getBoundingClientRect() : undefined;
498
+ return rect === undefined || rect.width > 0 || rect.height > 0;
499
+ };
500
+ const boundedQuery = (root, selector, maxMatched = 4096, maxVisited = 4096) => {
501
+ const simpleMatch = (element, token) => {
502
+ let offset = 0;
503
+ const tag = /^[A-Za-z][A-Za-z0-9-]*/u.exec(token);
504
+ if (tag !== null) {
505
+ if (element.tagName.toLocaleLowerCase() !== tag[0].toLocaleLowerCase())
506
+ return false;
507
+ offset = tag[0].length;
508
+ }
509
+ while (offset < token.length) {
510
+ if (token[offset] !== "[")
511
+ return false;
512
+ const close = token.indexOf("]", offset + 1);
513
+ if (close < 0)
514
+ return false;
515
+ const expression = token.slice(offset + 1, close).trim();
516
+ const attribute = /^([A-Za-z0-9_:-]+)(?:(\*=|=)'([^']*)'(?:\s+(i))?)?$/u.exec(expression);
517
+ if (attribute === null)
518
+ return false;
519
+ const actual = element.getAttribute(attribute[1]);
520
+ if (attribute[2] === undefined) {
521
+ if (actual === null)
522
+ return false;
523
+ }
524
+ else {
525
+ if (actual === null)
526
+ return false;
527
+ const insensitive = attribute[4] === "i";
528
+ const left = insensitive ? actual.toLocaleLowerCase() : actual;
529
+ const rightValue = attribute[3] ?? "";
530
+ const right = insensitive ? rightValue.toLocaleLowerCase() : rightValue;
531
+ if (attribute[2] === "=" ? left !== right : !left.includes(right))
532
+ return false;
533
+ }
534
+ offset = close + 1;
535
+ }
536
+ return true;
537
+ };
538
+ const tokensFor = (branch) => {
539
+ const tokens = [];
540
+ let depth = 0;
541
+ let start = 0;
542
+ for (let index = 0; index <= branch.length; index += 1) {
543
+ const character = branch[index];
544
+ if (character === "[")
545
+ depth += 1;
546
+ if (character === "]")
547
+ depth -= 1;
548
+ if ((character === undefined || /\s/u.test(character)) && depth === 0) {
549
+ const token = branch.slice(start, index).trim();
550
+ if (token.length > 0)
551
+ tokens.push(token);
552
+ start = index + 1;
553
+ }
554
+ }
555
+ return tokens;
556
+ };
557
+ const selectorMatch = (element) => {
558
+ for (const rawBranch of selector.split(",")) {
559
+ const tokens = tokensFor(rawBranch.trim());
560
+ if (tokens.length === 0 || !simpleMatch(element, tokens[tokens.length - 1]))
561
+ continue;
562
+ let ancestor = element.parentNode;
563
+ let tokenIndex = tokens.length - 2;
564
+ while (tokenIndex >= 0) {
565
+ while (ancestor !== null
566
+ && (ancestor.nodeType !== 1 || !simpleMatch(ancestor, tokens[tokenIndex]))) {
567
+ ancestor = ancestor.parentNode;
568
+ }
569
+ if (ancestor === null)
570
+ break;
571
+ tokenIndex -= 1;
572
+ ancestor = ancestor.parentNode;
573
+ }
574
+ if (tokenIndex < 0)
575
+ return true;
576
+ }
577
+ return false;
578
+ };
579
+ let visited = 0;
580
+ const matches = [];
581
+ let current = root.firstChild;
582
+ while (current !== null) {
583
+ visited += 1;
584
+ if (visited > maxVisited)
585
+ throw new Error("node limit exceeded");
586
+ if (current.nodeType === 1 && selectorMatch(current)) {
587
+ matches.push(current);
588
+ if (matches.length > maxMatched)
589
+ throw new Error("node limit exceeded");
590
+ }
591
+ if (current.firstChild !== null) {
592
+ current = current.firstChild;
593
+ continue;
594
+ }
595
+ while (current !== null && current !== root && current.nextSibling === null)
596
+ current = current.parentNode;
597
+ current = current === null || current === root ? null : current.nextSibling;
598
+ }
599
+ return matches;
600
+ };
601
+ const textboxes = boundedQuery(document, "textarea, [contenteditable='true'], [role='textbox']").filter(visible);
602
+ const composerAncestor = (textbox) => {
603
+ let fallback = null;
604
+ let current = textbox;
605
+ for (let depth = 0; current !== null && depth < 4096; depth += 1) {
606
+ if (current.nodeType === 1) {
607
+ const element = current;
608
+ if (element.tagName === "FORM")
609
+ return element;
610
+ const testId = (element.getAttribute("data-testid") ?? "").toLocaleLowerCase();
611
+ const classTokens = (element.getAttribute("class") ?? "").toLocaleLowerCase().split(/\s+/u);
612
+ if (fallback === null && (testId.includes("composer")
613
+ || classTokens.includes("composer-parent")
614
+ || classTokens.includes("group/composer")))
615
+ fallback = element;
616
+ }
617
+ current = current.parentNode;
618
+ }
619
+ if (current !== null)
620
+ throw new Error("node limit exceeded");
621
+ return fallback;
622
+ };
623
+ const composers = [...new Set(textboxes.map(textbox => composerAncestor(textbox)).filter((value) => value !== null))];
624
+ if (composers.length !== 1)
625
+ return { supported: false, count: 0, visibleAttachmentCount: 0 };
626
+ const inputs = boundedQuery(composers[0], "input[type='file']")
627
+ .filter(input => !input.disabled && input.getAttribute("aria-disabled") !== "true");
628
+ if (inputs.length !== 1)
629
+ return { supported: false, count: 0, visibleAttachmentCount: 0 };
630
+ const selectors = [
631
+ "[data-testid*='attachment' i]",
632
+ "[data-testid*='file' i]",
633
+ "[aria-label*='attachment' i]",
634
+ "[aria-label*='upload' i]",
635
+ "[aria-label*='file' i]",
636
+ "[class*='attachment' i]",
637
+ "[class*='upload' i]",
638
+ "[class*='file' i]",
639
+ "[role='progressbar']"
640
+ ].join(", ");
641
+ const visibleAttachmentCount = boundedQuery(composers[0], selectors).filter(visible).length;
642
+ return {
643
+ supported: true,
644
+ count: inputs[0].files?.length ?? 0,
645
+ visibleAttachmentCount
646
+ };
647
+ });
648
+ if (result === null || typeof result !== "object")
649
+ return undefined;
650
+ if (typeof result.supported !== "boolean" || !Number.isSafeInteger(result.count) || !Number.isSafeInteger(result.visibleAttachmentCount))
651
+ return undefined;
652
+ return result;
653
+ }
654
+ catch {
655
+ return undefined;
656
+ }
657
+ }
658
+ async function readComposerState(page, target, operationId, evidenceDigest) {
659
+ void target;
660
+ const locator = await uniqueVisibleLocator(page, composerTextbox);
661
+ if (locator === undefined)
662
+ return undefined;
663
+ const text = await readLocatorText(locator);
664
+ if (text === undefined || text.length > MAX_COMPOSER_CHARS || text.includes("\u0000"))
665
+ return undefined;
666
+ const currentStateDigest = safeDigestWith(evidenceDigest, "composer-state", {
667
+ operationId,
668
+ text
669
+ });
670
+ if (currentStateDigest === undefined)
671
+ return undefined;
672
+ const observationDigest = safeDigestWith(evidenceDigest, "composer-observation", {
673
+ operationId,
674
+ currentStateDigest
675
+ });
676
+ if (observationDigest === undefined)
677
+ return undefined;
678
+ return { text, currentStateDigest, evidenceDigest: observationDigest };
679
+ }
680
+ async function readLocatorText(locator) {
681
+ try {
682
+ // The locator textContent/innerText fallbacks expose no provider-side
683
+ // maximum and would materialize an unbounded string before this adapter
684
+ // could inspect it. The transactional path therefore requires evaluate
685
+ // so the cap is enforced inside the browser realm.
686
+ if (typeof locator.evaluate !== "function")
687
+ return undefined;
688
+ const value = await locator.evaluate(element => {
689
+ const candidate = element;
690
+ const candidateValue = candidate.value;
691
+ const tag = typeof candidate.tagName === "string" ? candidate.tagName.toLowerCase() : "";
692
+ // The Chrome bridge presents a synthetic empty `value` on ChatGPT's
693
+ // contenteditable DIV. Only native value controls use that property;
694
+ // contenteditable composers must be read from their bounded text tree.
695
+ if ((tag === "input" || tag === "textarea" || tag === "select") && typeof candidateValue === "string") {
696
+ // Enforce the cap in the browser realm before the bridge serializes
697
+ // the value. A post-return check would already have crossed the
698
+ // unbounded provider boundary.
699
+ return candidateValue.length <= 8 * 1024 * 1024 ? candidateValue : undefined;
700
+ }
701
+ const chunks = [];
702
+ const ancestors = [];
703
+ let visited = 0;
704
+ let total = 0;
705
+ let current = candidate;
706
+ while (current !== null) {
707
+ visited += 1;
708
+ if (visited > 4096)
709
+ return undefined;
710
+ if (current.nodeType === 3) {
711
+ const text = current.nodeValue ?? "";
712
+ total += text.length;
713
+ if (total > 8 * 1024 * 1024)
714
+ return undefined;
715
+ if (text.length > 0)
716
+ chunks.push(text);
717
+ }
718
+ const child = current.firstChild;
719
+ if (child !== null) {
720
+ if (ancestors.length >= 4096)
721
+ return undefined;
722
+ ancestors.push(current);
723
+ current = child;
724
+ continue;
725
+ }
726
+ while (current !== null && current !== candidate && current.nextSibling === null) {
727
+ current = ancestors.pop() ?? null;
728
+ }
729
+ if (current === candidate)
730
+ break;
731
+ if (current !== null)
732
+ current = current.nextSibling;
733
+ }
734
+ return chunks.join("");
735
+ });
736
+ return typeof value === "string" && value.length <= MAX_COMPOSER_CHARS ? value : undefined;
737
+ }
738
+ catch {
739
+ return undefined;
740
+ }
741
+ return undefined;
742
+ }
743
+ async function uniqueVisibleLocator(page, factory) {
744
+ if (typeof page.getByRole !== "function")
745
+ return undefined;
746
+ let locator;
747
+ try {
748
+ locator = factory(page);
749
+ }
750
+ catch {
751
+ return undefined;
752
+ }
753
+ if (typeof locator.count !== "function")
754
+ return undefined;
755
+ let count;
756
+ try {
757
+ count = await locator.count();
758
+ }
759
+ catch {
760
+ return undefined;
761
+ }
762
+ if (!Number.isSafeInteger(count) || count < 1 || count > MAX_LOCATOR_CANDIDATES)
763
+ return undefined;
764
+ const visible = [];
765
+ for (let index = 0; index < count; index += 1) {
766
+ const candidate = count === 1 ? locator : locator.nth?.(index);
767
+ if (candidate === undefined || typeof candidate.isVisible !== "function")
768
+ return undefined;
769
+ try {
770
+ if (await candidate.isVisible())
771
+ visible.push(candidate);
772
+ }
773
+ catch {
774
+ return undefined;
775
+ }
776
+ }
777
+ return visible.length === 1 ? visible[0] : undefined;
778
+ }
779
+ function targetForObservation(expected, state) {
780
+ void expected;
781
+ return state.target === undefined ? undefined : buildObservationTarget(state.target, state.authoritativeTabClaim);
782
+ }
783
+ function sendIdentity(state, expected) {
784
+ if (state.operationId === undefined || !isId(state.operationId) || state.requestDigest === undefined || !isDigest(state.requestDigest))
785
+ return undefined;
786
+ if (!isDigest(expected.targetBindingDigest))
787
+ return undefined;
788
+ return state.operationId;
789
+ }
790
+ function stableBaseline(snapshot) {
791
+ return snapshot.completeness === "complete"
792
+ && snapshot.userTurns.every(turn => turn.stableId !== undefined);
793
+ }
794
+ function baselineForSnapshot(snapshot, evidenceDigest, operationId, targetBindingDigest) {
795
+ if (snapshot.completeness !== "complete" || !stableBaseline(snapshot))
796
+ return undefined;
797
+ const snapshotDigest = safeDigestWith(evidenceDigest, "send-baseline-snapshot", {
798
+ operationId,
799
+ targetBindingDigest,
800
+ snapshotDigest: snapshot.snapshotDigest
801
+ }) ?? snapshot.snapshotDigest;
802
+ return {
803
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
804
+ snapshotDigest,
805
+ target: snapshot.target,
806
+ userTurns: snapshot.userTurns,
807
+ assistantTurns: snapshot.assistantTurns,
808
+ completeness: "complete"
809
+ };
810
+ }
811
+ function recoverPendingBlankBaseline(state) {
812
+ const target = state.target;
813
+ if (target === undefined
814
+ || target.targetLifecycle !== "new_pending"
815
+ || target.blankTaskEvidenceDigest === undefined
816
+ || !isDigest(target.blankTaskEvidenceDigest))
817
+ return undefined;
818
+ const observationTarget = buildObservationTarget(target, state.authoritativeTabClaim);
819
+ if (observationTarget === undefined)
820
+ return undefined;
821
+ const available = (value) => ({ status: "available", value });
822
+ const unavailable = () => ({ status: "unavailable", reason: "not_observed" });
823
+ const targetEvidence = {
824
+ provider: available(observationTarget.providerId),
825
+ browser: available(observationTarget.browserId),
826
+ tab: available(observationTarget.tabId),
827
+ thread: unavailable(),
828
+ conversation: unavailable(),
829
+ canonicalThreadUrl: unavailable(),
830
+ authoritativeTabClaim: observationTarget.authoritativeTabClaim === undefined
831
+ ? { status: "unavailable", reason: "not_exposed" }
832
+ : available(observationTarget.authoritativeTabClaim),
833
+ coordinationScope: observationTarget.coordinationScope
834
+ };
835
+ return {
836
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
837
+ snapshotDigest: target.blankTaskEvidenceDigest,
838
+ target: targetEvidence,
839
+ userTurns: [],
840
+ assistantTurns: [],
841
+ completeness: "complete"
842
+ };
843
+ }
844
+ function buildObservationTarget(target, authoritativeTabClaim) {
845
+ if (!isId(target.providerId)
846
+ || !isId(target.browserId)
847
+ || !isId(target.tabId)) {
848
+ return undefined;
849
+ }
850
+ const lifecycle = target.targetLifecycle ?? "fixed";
851
+ if (lifecycle !== "new_pending" && (target.conversationId === undefined
852
+ || !isId(target.conversationId)
853
+ || target.canonicalThreadUrl === undefined
854
+ || !OPAQUE_THREAD_URL_PATTERN.test(target.canonicalThreadUrl)))
855
+ return undefined;
856
+ if (lifecycle === "new_pending" && (target.newTargetAnchorDigest === undefined
857
+ || target.blankTaskEvidenceDigest === undefined
858
+ || !isDigest(target.newTargetAnchorDigest)
859
+ || !isDigest(target.blankTaskEvidenceDigest)))
860
+ return undefined;
861
+ if (target.coordinationScope === "provider" && (authoritativeTabClaim === undefined || !isId(authoritativeTabClaim)))
862
+ return undefined;
863
+ return {
864
+ providerId: target.providerId,
865
+ browserId: target.browserId,
866
+ tabId: target.tabId,
867
+ coordinationScope: target.coordinationScope,
868
+ targetLifecycle: lifecycle,
869
+ ...(authoritativeTabClaim === undefined ? {} : { authoritativeTabClaim }),
870
+ ...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId })
871
+ };
872
+ }
873
+ function makeObservationTarget(target, state) {
874
+ return buildObservationTarget(target, state.authoritativeTabClaim);
875
+ }
876
+ async function readCollectorContext(request, page, target, evidenceDigest, state) {
877
+ if (request.submissionActionId === undefined || !isId(request.submissionActionId))
878
+ throw new ProductionPrimitiveError("submission_witness_unwired");
879
+ // The service projects the authenticated causal baseline into every
880
+ // collect attempt. Use it for this context read as well, so a later
881
+ // observation can request terminal metadata for the exact assistant turn
882
+ // even when a previous wait:false call returned to the caller.
883
+ const baseline = request.baseline;
884
+ if (baseline === undefined)
885
+ throw new ProductionPrimitiveError("target_evidence_unavailable");
886
+ const observationTarget = makeObservationTarget(target, state);
887
+ if (observationTarget === undefined)
888
+ throw new ProductionPrimitiveError("target_evidence_unavailable");
889
+ const observation = await observeBrowserPage(page, {
890
+ operationId: request.operationId,
891
+ target: observationTarget,
892
+ evidenceDigest,
893
+ responseContent: "metadata",
894
+ baseline
895
+ });
896
+ const binding = {
897
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
898
+ operationId: request.operationId,
899
+ targetBindingDigest: request.targetBindingDigest,
900
+ target: observation.snapshot.target,
901
+ evidenceProfile: {
902
+ stableConversationId: "required",
903
+ stableUserTurnId: "required",
904
+ stableAssistantTurnId: "required",
905
+ stableBranchId: "required",
906
+ authoritativeTabClaim: target.coordinationScope === "provider" ? "required" : "unavailable"
907
+ },
908
+ replacementTabRecovery: false,
909
+ actionId: request.submissionActionId,
910
+ actionKind: request.submissionActionKind ?? "send"
911
+ };
912
+ // This cursor is only a candidate from the immediately preceding read. The
913
+ // service keeps the journal baseline/witness authoritative, and the
914
+ // collector reclassifies the next snapshot against both before using it.
915
+ // Never fabricate a cursor when the authenticated witness is absent or the
916
+ // exact delta cannot be classified.
917
+ let prior;
918
+ if (request.submissionWitness !== undefined) {
919
+ try {
920
+ prior = classifyTurnOwnership({
921
+ binding,
922
+ baseline,
923
+ snapshot: observation.snapshot,
924
+ submissionWitness: request.submissionWitness
925
+ }).cursor;
926
+ }
927
+ catch {
928
+ prior = undefined;
929
+ }
930
+ }
931
+ return {
932
+ binding,
933
+ baseline,
934
+ ...(prior === undefined ? {} : { prior })
935
+ };
936
+ }
937
+ async function observeCollector(request, page, target, context, evidenceDigest, state) {
938
+ const observationTarget = makeObservationTarget(target, state);
939
+ if (observationTarget === undefined)
940
+ throw new ProductionPrimitiveError("target_evidence_unavailable");
941
+ const result = await observeBrowserPage(page, {
942
+ operationId: request.operationId,
943
+ target: observationTarget,
944
+ evidenceDigest,
945
+ responseContent: request.responseContent,
946
+ ...(context.baseline === undefined ? {} : { baseline: context.baseline }),
947
+ ...(context.prior?.assistantTurnId === undefined ? {} : {
948
+ terminalAssistantTurnId: context.prior.assistantTurnId,
949
+ ...(request.responseContent === "include" ? { rawAssistantTurnId: context.prior.assistantTurnId } : {})
950
+ })
951
+ });
952
+ return {
953
+ schemaVersion: COLLECTOR_SCHEMA_VERSION,
954
+ snapshot: result.snapshot,
955
+ ...(result.terminal === undefined ? {} : { terminal: result.terminal })
956
+ };
957
+ }
958
+ async function sleepOutsideBrowser(milliseconds, signal) {
959
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 60_000)
960
+ throw new ProductionPrimitiveError("invalid_sleep");
961
+ if (signal.aborted)
962
+ throw new ProductionPrimitiveError("operation_cancelled");
963
+ await new Promise((resolve, reject) => {
964
+ const timer = setTimeout(() => {
965
+ signal.removeEventListener("abort", onAbort);
966
+ resolve();
967
+ }, milliseconds);
968
+ const onAbort = () => {
969
+ clearTimeout(timer);
970
+ signal.removeEventListener("abort", onAbort);
971
+ reject(new ProductionPrimitiveError("operation_cancelled"));
972
+ };
973
+ signal.addEventListener("abort", onAbort, { once: true });
974
+ });
975
+ }
976
+ async function observeControlTurn(request, page, target, evidenceDigest, state) {
977
+ const observation = await observeControlSnapshot(request.operationId, page, target, evidenceDigest, state);
978
+ if (observation === undefined)
979
+ return { status: "uncertain", reason: "unavailable" };
980
+ const assistant = observation.snapshot.assistantTurns.find(turn => turn.stableId === request.expectedAssistantTurnId);
981
+ if (assistant === undefined)
982
+ return { status: "mismatch", reason: "different_turn", evidenceDigest: observation.snapshot.snapshotDigest };
983
+ if (assistant.state === "generating") {
984
+ return { status: "generating", assistantTurnId: request.expectedAssistantTurnId, evidenceDigest: assistant.evidenceDigest };
985
+ }
986
+ return { status: "terminal", assistantTurnId: request.expectedAssistantTurnId, reason: "not_generating", evidenceDigest: assistant.evidenceDigest };
987
+ }
988
+ async function executeControlOnce(request, page, target, evidenceDigest, state) {
989
+ if (request.action !== "stop")
990
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
991
+ const locator = await uniqueVisibleLocator(page, stopGenerationButton);
992
+ if (locator === undefined || typeof locator.click !== "function")
993
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
994
+ try {
995
+ // Sole Stop activation. A rejection is not retried and is reconciled by
996
+ // the control coordinator's observation-only path.
997
+ await locator.click();
998
+ }
999
+ catch {
1000
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
1001
+ }
1002
+ // Release the mutation actor before reading the postcondition. Returning an
1003
+ // uncertain result instructs the control coordinator to reacquire the tab
1004
+ // through its observation-only port; it must never make this primitive hold
1005
+ // the actor across both activation and reconciliation.
1006
+ return { status: "uncertain" };
1007
+ }
1008
+ async function observeControlPostcondition(request, page, target, evidenceDigest, state) {
1009
+ if (request.action !== "stop")
1010
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
1011
+ const observation = await observeControlSnapshot(request.operationId, page, target, evidenceDigest, state);
1012
+ if (observation === undefined)
1013
+ return { status: "uncertain", blockerCode: "target_evidence_unavailable" };
1014
+ const assistant = observation.snapshot.assistantTurns.find(turn => turn.stableId === request.expectedAssistantTurnId);
1015
+ if (assistant === undefined)
1016
+ return { status: "not_satisfied", blockerCode: "target_binding_mismatch", evidenceDigest: observation.snapshot.snapshotDigest };
1017
+ if (assistant.state === "generating")
1018
+ return { status: "not_satisfied", blockerCode: "send_control_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
1019
+ return { status: "satisfied", assistantTurnId: request.expectedAssistantTurnId, evidenceDigest: assistant.evidenceDigest };
1020
+ }
1021
+ async function observeControlSnapshot(operationId, page, target, evidenceDigest, state) {
1022
+ const observationTarget = makeObservationTarget(target, state);
1023
+ if (observationTarget === undefined)
1024
+ return undefined;
1025
+ try {
1026
+ return await observeBrowserPage(page, {
1027
+ operationId,
1028
+ target: observationTarget,
1029
+ evidenceDigest,
1030
+ responseContent: "metadata"
1031
+ });
1032
+ }
1033
+ catch {
1034
+ return undefined;
1035
+ }
1036
+ }
1037
+ function safeDigestWith(evidenceDigest, domain, material) {
1038
+ try {
1039
+ const value = evidenceDigest(domain, material);
1040
+ return isDigest(value) ? value : undefined;
1041
+ }
1042
+ catch {
1043
+ return undefined;
1044
+ }
1045
+ }
1046
+ function digest(evidenceDigest, domain, material) {
1047
+ return safeDigestWith(evidenceDigest, domain, material);
1048
+ }
1049
+ function isDigest(value) {
1050
+ return typeof value === "string" && DIGEST_PATTERN.test(value);
1051
+ }
1052
+ function isId(value) {
1053
+ return typeof value === "string" && ID_PATTERN.test(value);
1054
+ }
1055
+ function validateOptions(options) {
1056
+ if (options === null || typeof options !== "object" || Array.isArray(options) || typeof options.evidenceDigest !== "function") {
1057
+ throw new ProductionPrimitiveError("invalid_options");
1058
+ }
1059
+ if (options.operationId !== undefined && !isId(options.operationId))
1060
+ throw new ProductionPrimitiveError("invalid_options");
1061
+ if (options.requestDigest !== undefined && !isDigest(options.requestDigest))
1062
+ throw new ProductionPrimitiveError("invalid_options");
1063
+ if (options.authoritativeTabClaim !== undefined && !isId(options.authoritativeTabClaim))
1064
+ throw new ProductionPrimitiveError("invalid_options");
1065
+ if (options.desiredComposerText !== undefined && typeof options.desiredComposerText !== "string")
1066
+ throw new ProductionPrimitiveError("invalid_options");
1067
+ if (options.composerText !== undefined && typeof options.composerText !== "string")
1068
+ throw new ProductionPrimitiveError("invalid_options");
1069
+ if (options.observeAttachments !== undefined && typeof options.observeAttachments !== "function")
1070
+ throw new ProductionPrimitiveError("invalid_options");
1071
+ if (options.target !== undefined && (options.target === null || typeof options.target !== "object" || Array.isArray(options.target)))
1072
+ throw new ProductionPrimitiveError("invalid_options");
1073
+ }