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,856 @@
1
+ import { composerTextbox, stopGenerationButton } from "../dom/selectors.js";
2
+ import { inspectComposerText } from "../dom/composer-text.js";
3
+ import { readChatGPTEmptyAttachmentState } from "./production-chatgpt-attachments.js";
4
+ import { observeBrowserPage } from "./browser-observation.js";
5
+ import { COLLECTOR_SCHEMA_VERSION } from "./collector.js";
6
+ import { classifyTurnOwnership, TURN_OWNERSHIP_SCHEMA_VERSION, } from "./turn-ownership.js";
7
+ /**
8
+ * A small, provider-facing primitive layer for the transactional adapter.
9
+ *
10
+ * This module intentionally does not call a legacy command. Each callback
11
+ * performs a bounded locator/evaluation operation, or one browser activation,
12
+ * and returns only keyed evidence. Attachment handoff, configuration,
13
+ * The base factory deliberately leaves configuration, attachments, and Work
14
+ * steer to their dedicated provider modules. `chatgpt-runtime.ts` composes
15
+ * those modules into the default ChatGPT operation runtime; the inventory
16
+ * below describes this base factory, not the completed composite runtime.
17
+ */
18
+ const DIGEST_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/u;
19
+ const ID_PATTERN = /^[A-Za-z0-9._:-]{1,512}$/u;
20
+ const OPAQUE_THREAD_URL_PATTERN = /^https:\/\/opaque\.invalid\/thread\/[0-9a-f]{64}$/u;
21
+ const MAX_LOCATOR_CANDIDATES = 128;
22
+ const MAX_COMPOSER_CHARS = 8 * 1024 * 1024;
23
+ export const PRODUCTION_PRIMITIVE_CAPABILITIES = Object.freeze([
24
+ "composer_set",
25
+ "empty_attachment_observation",
26
+ "send_activation",
27
+ "collector_snapshot",
28
+ "durable_baseline_projection",
29
+ "submission_witness_recovery",
30
+ "stop_control"
31
+ ]);
32
+ /**
33
+ * These are not soft feature flags. They are an inventory of deliberately
34
+ * missing proof, so a caller cannot mistake an unavailable primitive for a
35
+ * best-effort browser fallback.
36
+ */
37
+ export const UNWIRED_PRODUCTION_PRIMITIVES = Object.freeze([
38
+ "configuration_set",
39
+ "tool_selection",
40
+ "power_select",
41
+ "file_chooser_handoff",
42
+ "attachment_identity_for_nonempty_manifest",
43
+ "work_steer_activation"
44
+ ]);
45
+ export const PRODUCTION_OPERATION_PRIMITIVE_INVENTORY = Object.freeze({
46
+ scope: "base_primitive_factory",
47
+ wired: PRODUCTION_PRIMITIVE_CAPABILITIES,
48
+ unwired: UNWIRED_PRODUCTION_PRIMITIVES
49
+ });
50
+ export class ProductionPrimitiveError extends Error {
51
+ code;
52
+ constructor(code) {
53
+ super("The provider-specific operation primitive could not prove the requested action safely.");
54
+ this.code = code;
55
+ this.name = "ProductionPrimitiveError";
56
+ }
57
+ }
58
+ /**
59
+ * Create one request-scoped set of production operation primitives.
60
+ *
61
+ * `operationId`, `requestDigest`, and the composer value should normally be
62
+ * supplied by the lazy runtime capture after the journal has created the
63
+ * operation. If either identity is absent, Send fails closed rather than
64
+ * fabricating an evidence domain.
65
+ */
66
+ export function createProductionOperationPrimitives(options) {
67
+ validateOptions(options);
68
+ const state = {
69
+ ...(options.operationId === undefined ? {} : { operationId: options.operationId }),
70
+ ...(options.requestDigest === undefined ? {} : { requestDigest: options.requestDigest }),
71
+ ...(options.desiredComposerText !== undefined
72
+ ? { desiredComposerText: options.desiredComposerText }
73
+ : options.composerText === undefined ? {} : { desiredComposerText: options.composerText }),
74
+ ...(options.authoritativeTabClaim === undefined ? {} : { authoritativeTabClaim: options.authoritativeTabClaim }),
75
+ ...(options.target === undefined ? {} : { target: options.target })
76
+ };
77
+ const staging = Object.freeze({
78
+ readCurrent: request => readStaging(request, options.evidenceDigest, state),
79
+ mutateOnce: request => mutateStagingOnce(request, options.evidenceDigest, state),
80
+ observe: request => readStaging(request, options.evidenceDigest, state)
81
+ });
82
+ const sendObservers = Object.freeze({
83
+ observePrecondition: request => observeSendPrecondition(request, options.evidenceDigest, state, options.observeAttachments),
84
+ observePostcondition: async (request) => {
85
+ const result = await observeSendPostcondition(request, options.evidenceDigest, state);
86
+ return result.status === "blocked"
87
+ && (result.blockerCode === "ambiguous_submit" || result.blockerCode === "target_evidence_unavailable")
88
+ ? { result, retryable: true }
89
+ : result;
90
+ },
91
+ // Every retry is an observation-only transaction. The delay remains
92
+ // outside the tab actor and can never repeat the Send activation.
93
+ sleep: sleepOutsideBrowser,
94
+ // ChatGPT can establish the canonical conversation URL before it exposes
95
+ // stable user/assistant DOM identities. Keep that provider settling window
96
+ // bounded without collapsing a successful one-shot Send into uncertainty.
97
+ maxPostconditionAttempts: 32,
98
+ postconditionIntervalMs: 250,
99
+ postconditionTimeoutMs: 15_000
100
+ });
101
+ const submission = Object.freeze({
102
+ observeStaging: (request, page, target) => observeSubmissionStaging(request, page, target, options.evidenceDigest, state),
103
+ // The file identity layer revalidates paths outside the actor. There is
104
+ // still no identity-grade provider chooser primitive here, so never begin
105
+ // a chooser mutation under an invented selector or wait loop.
106
+ handoffFiles: async () => ({
107
+ status: "not_satisfied",
108
+ blockerCode: "attachment_manifest_mismatch"
109
+ }),
110
+ observeAttachments: (request, page, target) => observeSubmissionAttachments(request, page, target, options.evidenceDigest, state, options.observeAttachments),
111
+ sendObservers
112
+ });
113
+ const collector = Object.freeze({
114
+ readContext: (request, page, target) => readCollectorContext(request, page, target, options.evidenceDigest, state),
115
+ observe: (request, page, target, context) => observeCollector(request, page, target, context, options.evidenceDigest, state),
116
+ // This timer is intentionally outside any tab transaction. The browser
117
+ // adapter invokes it after its short observation transaction has settled.
118
+ sleep: sleepOutsideBrowser
119
+ });
120
+ const control = Object.freeze({
121
+ observeTurn: (request, page, target) => observeControlTurn(request, page, target, options.evidenceDigest, state),
122
+ executeOnce: (request, page, target) => executeControlOnce(request, page, target, options.evidenceDigest, state),
123
+ observePostcondition: (request, page, target) => observeControlPostcondition(request, page, target, options.evidenceDigest, state)
124
+ });
125
+ return Object.freeze({
126
+ staging,
127
+ submission,
128
+ collector,
129
+ control
130
+ });
131
+ }
132
+ /** Descriptive aliases for integrations that name the layer after the adapter. */
133
+ export const createOperationProductionPrimitives = createProductionOperationPrimitives;
134
+ export const createProductionPrimitives = createProductionOperationPrimitives;
135
+ async function readStaging(request, evidenceDigest, state) {
136
+ if (request.kind !== "composer_set") {
137
+ return unavailableStaging(request, stagingUnwiredCode(request.kind));
138
+ }
139
+ if (state.desiredComposerText === undefined) {
140
+ return unavailableStaging(request, "composer_primitive_unwired");
141
+ }
142
+ if (!await matchesExpectedStagingDigest(request, evidenceDigest)) {
143
+ return unavailableStaging(request, "composer_request_mismatch");
144
+ }
145
+ const current = await readComposerState(request.page, request.target, request.operationId, evidenceDigest);
146
+ if (current === undefined)
147
+ return unavailableStaging(request, "composer_control_unavailable");
148
+ const satisfied = current.text === state.desiredComposerText;
149
+ const evidence = await digest(evidenceDigest, "composer-observation", {
150
+ operationId: request.operationId,
151
+ targetBindingDigest: request.targetBindingDigest,
152
+ currentStateDigest: current.currentStateDigest,
153
+ status: satisfied ? "satisfied" : "not_satisfied"
154
+ });
155
+ if (evidence === undefined)
156
+ return unavailableStaging(request, "evidence_digest_failed");
157
+ return {
158
+ status: satisfied ? "satisfied" : "not_satisfied",
159
+ desiredStateDigest: request.desiredStateDigest,
160
+ currentStateDigest: current.currentStateDigest,
161
+ evidenceDigest: evidence
162
+ };
163
+ }
164
+ async function mutateStagingOnce(request, evidenceDigest, state) {
165
+ if (request.kind !== "composer_set" || state.desiredComposerText === undefined) {
166
+ throw new ProductionPrimitiveError(request.kind === "composer_set" ? "composer_primitive_unwired" : stagingUnwiredCode(request.kind));
167
+ }
168
+ if (!isDigest(request.desiredStateDigest) || !isDigest(request.requestDigest)) {
169
+ throw new ProductionPrimitiveError("composer_request_mismatch");
170
+ }
171
+ const expected = await safeDigestWith(evidenceDigest, "staging-desired", { requestDigest: request.requestDigest, kind: request.kind });
172
+ if (expected === undefined || expected !== request.desiredStateDigest) {
173
+ throw new ProductionPrimitiveError("composer_request_mismatch");
174
+ }
175
+ assertStagingActive(request);
176
+ const locator = await uniqueVisibleLocator(request.page, composerTextbox);
177
+ if (locator === undefined || typeof locator.fill !== "function") {
178
+ throw new ProductionPrimitiveError("composer_control_unavailable");
179
+ }
180
+ // The sole reversible composer mutation. There is no readiness wait and no
181
+ // fallback press/click path if this call rejects.
182
+ assertStagingActive(request);
183
+ await locator.fill(state.desiredComposerText);
184
+ return { status: "started" };
185
+ }
186
+ function assertStagingActive(request) {
187
+ if (request.signal?.aborted)
188
+ throw new ProductionPrimitiveError("operation_cancelled");
189
+ if (request.deadlineAt !== undefined && Date.now() >= request.deadlineAt)
190
+ throw new ProductionPrimitiveError("operation_deadline_exceeded");
191
+ }
192
+ function stagingUnwiredCode(kind) {
193
+ switch (kind) {
194
+ case "configuration_set": return "configuration_primitive_unwired";
195
+ case "tool_set": return "tool_primitive_unwired";
196
+ case "power_select": return "power_primitive_unwired";
197
+ case "composer_set": return "composer_primitive_unwired";
198
+ }
199
+ }
200
+ function unavailableStaging(request, blockerCode) {
201
+ return {
202
+ status: "unavailable",
203
+ desiredStateDigest: request.desiredStateDigest,
204
+ blockerCode
205
+ };
206
+ }
207
+ async function matchesExpectedStagingDigest(request, evidenceDigest) {
208
+ return await safeDigestWith(evidenceDigest, "staging-desired", {
209
+ requestDigest: request.requestDigest,
210
+ kind: request.kind
211
+ }) === request.desiredStateDigest;
212
+ }
213
+ async function observeSubmissionStaging(request, page, target, evidenceDigest, state) {
214
+ state.target = target;
215
+ if (!isDigest(request.configurationReceiptDigest) || !isDigest(request.composerReceiptDigest)) {
216
+ return { status: "unavailable", reason: "unknown" };
217
+ }
218
+ const expectedConfiguration = state.requestDigest === undefined
219
+ ? undefined
220
+ : await safeDigestWith(evidenceDigest, "configuration-request", state.requestDigest);
221
+ const expectedComposer = state.requestDigest === undefined
222
+ ? undefined
223
+ : await safeDigestWith(evidenceDigest, "composer-request", state.requestDigest);
224
+ if (expectedConfiguration === undefined || expectedComposer === undefined) {
225
+ return { status: "unavailable", reason: "target" };
226
+ }
227
+ if (request.configurationReceiptDigest !== expectedConfiguration) {
228
+ const evidence = await safeDigestWith(evidenceDigest, "submission-stage", { operationId: request.operationId, reason: "configuration" });
229
+ return evidence === undefined
230
+ ? { status: "mismatch", reason: "configuration" }
231
+ : { status: "mismatch", reason: "configuration", evidenceDigest: evidence };
232
+ }
233
+ if (request.composerReceiptDigest !== expectedComposer || state.desiredComposerText === undefined) {
234
+ return { status: "unavailable", reason: "composer" };
235
+ }
236
+ const current = await readComposerState(page, target, request.operationId, evidenceDigest);
237
+ if (current === undefined)
238
+ return { status: "unavailable", reason: "composer" };
239
+ if (current.text !== state.desiredComposerText) {
240
+ return { status: "mismatch", reason: "composer", evidenceDigest: current.evidenceDigest };
241
+ }
242
+ return {
243
+ status: "exact",
244
+ evidenceDigest: current.evidenceDigest
245
+ };
246
+ }
247
+ async function observeSendPrecondition(request, evidenceDigest, state, attachmentObserver) {
248
+ const identity = sendIdentity(state, request.expected);
249
+ if (identity === undefined)
250
+ return { status: "unavailable", code: "target_evidence_unavailable" };
251
+ const target = targetForObservation(request.expected, state);
252
+ if (target === undefined)
253
+ return { status: "unavailable", code: "target_evidence_unavailable" };
254
+ // Recovery after a durable Send intent is observation-only. A pending new
255
+ // target necessarily no longer renders the blank surface, so re-running the
256
+ // normal precondition would reject the very post-Send state we must inspect.
257
+ // The durable blank-task snapshot is the bounded baseline authority here;
258
+ // the subsequent postcondition still has to prove exactly one new user turn
259
+ // and complete provider conversation identity before establishment.
260
+ if (request.mode === "observe_only" && state.target?.targetLifecycle === "new_pending") {
261
+ const recoveryBaseline = recoverPendingBlankBaseline(state);
262
+ if (recoveryBaseline === undefined)
263
+ return { status: "unavailable", code: "target_evidence_unavailable" };
264
+ return {
265
+ status: "exact",
266
+ targetBindingDigest: request.expected.targetBindingDigest,
267
+ configurationReceiptDigest: request.expected.configurationReceiptDigest,
268
+ composerReceiptDigest: request.expected.composerReceiptDigest,
269
+ attachments: {
270
+ count: request.expected.attachmentManifest.count,
271
+ orderPolicy: "exact",
272
+ identityDigests: request.expected.attachmentManifest.identities.map(entry => entry.identityDigest)
273
+ },
274
+ baseline: {
275
+ ownershipBaseline: recoveryBaseline,
276
+ userTurnEvidenceDigest: await safeDigestWith(evidenceDigest, "send-baseline", {
277
+ snapshotDigest: recoveryBaseline.snapshotDigest,
278
+ userTurns: []
279
+ }) ?? recoveryBaseline.snapshotDigest
280
+ },
281
+ evidenceDigest: recoveryBaseline.snapshotDigest
282
+ };
283
+ }
284
+ if (state.desiredComposerText === undefined)
285
+ return { status: "unavailable", code: "composer_drift" };
286
+ const expectedComposer = await safeDigestWith(evidenceDigest, "composer-request", state.requestDigest);
287
+ const expectedConfiguration = await safeDigestWith(evidenceDigest, "configuration-request", state.requestDigest);
288
+ if (expectedComposer === undefined || expectedConfiguration === undefined) {
289
+ return { status: "unavailable", code: "target_evidence_unavailable" };
290
+ }
291
+ if (request.expected.composerReceiptDigest !== expectedComposer) {
292
+ return { status: "mismatch", code: "composer_drift" };
293
+ }
294
+ if (request.expected.configurationReceiptDigest !== expectedConfiguration) {
295
+ return { status: "mismatch", code: "configuration_drift" };
296
+ }
297
+ let observation;
298
+ try {
299
+ observation = await observeBrowserPage(request.page, {
300
+ operationId: identity,
301
+ target,
302
+ evidenceDigest,
303
+ responseContent: "metadata"
304
+ });
305
+ }
306
+ catch {
307
+ return { status: "unavailable", code: "target_evidence_unavailable" };
308
+ }
309
+ const snapshot = observation.snapshot;
310
+ if (state.target?.targetLifecycle === "new_pending") {
311
+ const anchor = observation.newTargetAnchor;
312
+ if (anchor === undefined
313
+ || anchor.anchorDigest !== state.target.newTargetAnchorDigest
314
+ || anchor.blankTaskEvidenceDigest !== state.target.blankTaskEvidenceDigest
315
+ || snapshot.target.thread.status !== "unavailable"
316
+ || snapshot.target.conversation.status !== "unavailable"
317
+ || snapshot.target.canonicalThreadUrl.status !== "unavailable"
318
+ || snapshot.userTurns.length !== 0
319
+ || snapshot.assistantTurns.length !== 0) {
320
+ return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
321
+ }
322
+ }
323
+ if (!stableBaseline(snapshot)) {
324
+ return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
325
+ }
326
+ const composerTarget = state.target;
327
+ if (composerTarget === undefined)
328
+ return { status: "unavailable", code: "target_evidence_unavailable" };
329
+ const composer = await readComposerState(request.page, composerTarget, identity, evidenceDigest);
330
+ if (composer === undefined)
331
+ return { status: "unavailable", code: "composer_drift", evidenceDigest: snapshot.snapshotDigest };
332
+ if (composer.text !== state.desiredComposerText) {
333
+ return { status: "mismatch", code: "composer_drift", evidenceDigest: composer.evidenceDigest };
334
+ }
335
+ const attachments = await observeAttachmentEnvelope({
336
+ operationId: identity,
337
+ requestDigest: state.requestDigest,
338
+ surface: request.expected.surface,
339
+ targetBindingDigest: request.expected.targetBindingDigest,
340
+ manifest: request.expected.attachmentManifest
341
+ }, request.page, composerTarget, evidenceDigest, state, attachmentObserver);
342
+ if (attachments.status !== "absent" && attachments.status !== "exact") {
343
+ return { status: "unavailable", code: "attachment_manifest_mismatch", evidenceDigest: snapshot.snapshotDigest };
344
+ }
345
+ const baseline = await baselineForSnapshot(snapshot, evidenceDigest, identity, request.expected.targetBindingDigest);
346
+ if (baseline === undefined)
347
+ return { status: "unavailable", code: "target_evidence_unavailable", evidenceDigest: snapshot.snapshotDigest };
348
+ const sendAttachments = {
349
+ count: attachments.count,
350
+ orderPolicy: "exact",
351
+ identityDigests: [...attachments.identityDigests]
352
+ };
353
+ return {
354
+ status: "exact",
355
+ targetBindingDigest: request.expected.targetBindingDigest,
356
+ configurationReceiptDigest: request.expected.configurationReceiptDigest,
357
+ composerReceiptDigest: request.expected.composerReceiptDigest,
358
+ attachments: sendAttachments,
359
+ baseline: {
360
+ ...(baseline.userTurns.at(-1)?.stableId === undefined ? {} : { userTurnId: baseline.userTurns.at(-1).stableId }),
361
+ ownershipBaseline: baseline,
362
+ userTurnEvidenceDigest: await safeDigestWith(evidenceDigest, "send-baseline", {
363
+ snapshotDigest: baseline.snapshotDigest,
364
+ userTurns: baseline.userTurns.map(turn => turn.evidenceDigest)
365
+ }) ?? baseline.snapshotDigest
366
+ },
367
+ evidenceDigest: snapshot.snapshotDigest
368
+ };
369
+ }
370
+ async function observeSendPostcondition(request, evidenceDigest, state) {
371
+ const identity = sendIdentity(state, request.expected);
372
+ if (identity === undefined)
373
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
374
+ // The SendOnce coordinator carries the complete baseline that was either
375
+ // durably appended before activation or projected by the service after a
376
+ // restart. Never fall back to the request-scoped map/current page here.
377
+ const baseline = request.baseline.ownershipBaseline;
378
+ if (baseline === undefined)
379
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
380
+ // A process restart loses the in-memory baseline map. The durable pending
381
+ // target's blank-task evidence is the only safe recovery prefix: it proves
382
+ // that the operation owned zero turns before its one durable Send intent.
383
+ const target = targetForObservation(request.expected, state);
384
+ if (target === undefined)
385
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
386
+ let observation;
387
+ try {
388
+ observation = await observeBrowserPage(request.page, {
389
+ operationId: identity,
390
+ target,
391
+ evidenceDigest,
392
+ responseContent: "metadata",
393
+ baseline
394
+ });
395
+ }
396
+ catch {
397
+ return { status: "blocked", blockerCode: "target_evidence_unavailable" };
398
+ }
399
+ const delta = observation.snapshot.postSendDelta;
400
+ if (delta === undefined || delta.baselineSnapshotDigest !== baseline.snapshotDigest) {
401
+ return { status: "blocked", blockerCode: "ambiguous_submit", evidenceDigest: observation.snapshot.snapshotDigest };
402
+ }
403
+ const added = observation.snapshot.userTurns.filter(turn => delta.addedUserEvidenceDigests.includes(turn.evidenceDigest));
404
+ if (added.length !== 1) {
405
+ return {
406
+ status: "blocked",
407
+ blockerCode: added.length > 1 ? "concurrent_user_turn" : "ambiguous_submit",
408
+ evidenceDigest: observation.snapshot.snapshotDigest
409
+ };
410
+ }
411
+ const user = added[0];
412
+ if (user?.stableId === undefined) {
413
+ return { status: "blocked", blockerCode: "target_evidence_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
414
+ }
415
+ const assistant = observation.snapshot.assistantTurns.find(turn => turn.parentStableId === user.stableId);
416
+ const status = request.mode === "observe_only" ? "already_submitted" : "submitted";
417
+ const established = state.target?.targetLifecycle === "new_pending"
418
+ ? (() => {
419
+ const conversation = observation.snapshot.target.conversation;
420
+ const canonicalThreadUrl = observation.snapshot.target.canonicalThreadUrl;
421
+ if (conversation.status !== "available"
422
+ || canonicalThreadUrl.status !== "available"
423
+ || state.target.newTargetAnchorDigest === undefined)
424
+ return undefined;
425
+ return {
426
+ targetBindingDigest: request.expected.targetBindingDigest,
427
+ anchorDigest: state.target.newTargetAnchorDigest,
428
+ causalSendActionId: request.actionId,
429
+ conversationId: conversation.value,
430
+ canonicalThreadUrl: canonicalThreadUrl.value,
431
+ userTurnId: user.stableId,
432
+ userTurnEvidenceDigest: user.evidenceDigest,
433
+ postSendDeltaDigest: delta.deltaDigest,
434
+ evidenceDigest: observation.snapshot.snapshotDigest
435
+ };
436
+ })()
437
+ : undefined;
438
+ if (state.target?.targetLifecycle === "new_pending" && established === undefined) {
439
+ return { status: "blocked", blockerCode: "target_evidence_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
440
+ }
441
+ return {
442
+ status,
443
+ targetBindingDigest: request.expected.targetBindingDigest,
444
+ evidenceDigest: observation.snapshot.snapshotDigest,
445
+ userTurnId: user.stableId,
446
+ userTurnEvidenceDigest: user.evidenceDigest,
447
+ postSendDeltaDigest: delta.deltaDigest,
448
+ ...(assistant?.stableId === undefined ? {} : { assistantTurnId: assistant.stableId }),
449
+ ...(established === undefined ? {} : { targetEstablishment: established })
450
+ };
451
+ }
452
+ async function observeSubmissionAttachments(request, page, target, evidenceDigest, state, attachmentObserver) {
453
+ state.target = target;
454
+ return await observeAttachmentEnvelope(request, page, target, evidenceDigest, state, attachmentObserver);
455
+ }
456
+ async function observeAttachmentEnvelope(request, page, target, evidenceDigest, state, attachmentObserver) {
457
+ if (attachmentObserver !== undefined && request.manifest.count > 0) {
458
+ try {
459
+ return await attachmentObserver(request, page, target);
460
+ }
461
+ catch {
462
+ return { status: "unavailable" };
463
+ }
464
+ }
465
+ if (request.manifest.count > 0)
466
+ return { status: "unavailable" };
467
+ const result = await readChatGPTEmptyAttachmentState(page);
468
+ if (result === undefined || !result.supported)
469
+ return { status: "unavailable" };
470
+ if (result.count !== 0 || result.visibleAttachmentCount !== 0)
471
+ return { status: "mismatch" };
472
+ const evidence = await safeDigestWith(evidenceDigest, "composer-attachments", {
473
+ operationId: request.operationId,
474
+ targetBindingDigest: request.targetBindingDigest,
475
+ count: 0
476
+ });
477
+ if (evidence === undefined)
478
+ return { status: "unavailable" };
479
+ return {
480
+ status: "absent",
481
+ evidenceDigest: evidence,
482
+ count: 0,
483
+ orderPolicy: "exact",
484
+ identityDigests: []
485
+ };
486
+ }
487
+ async function readComposerState(page, target, operationId, evidenceDigest) {
488
+ void target;
489
+ const locator = await uniqueVisibleLocator(page, composerTextbox);
490
+ if (locator === undefined)
491
+ return undefined;
492
+ const text = await readLocatorText(locator);
493
+ if (text === undefined || text.length > MAX_COMPOSER_CHARS || text.includes("\u0000"))
494
+ return undefined;
495
+ const currentStateDigest = await safeDigestWith(evidenceDigest, "composer-state", {
496
+ operationId,
497
+ text
498
+ });
499
+ if (currentStateDigest === undefined)
500
+ return undefined;
501
+ const observationDigest = await safeDigestWith(evidenceDigest, "composer-observation", {
502
+ operationId,
503
+ currentStateDigest
504
+ });
505
+ if (observationDigest === undefined)
506
+ return undefined;
507
+ return { text, currentStateDigest, evidenceDigest: observationDigest };
508
+ }
509
+ async function readLocatorText(locator) {
510
+ try {
511
+ // The locator textContent/innerText fallbacks expose no provider-side
512
+ // maximum and would materialize an unbounded string before this adapter
513
+ // could inspect it. The transactional path therefore requires evaluate
514
+ // so the cap is enforced inside the browser realm.
515
+ if (typeof locator.evaluate !== "function")
516
+ return undefined;
517
+ const value = await locator.evaluate(inspectComposerText);
518
+ return typeof value === "string" && value.length <= MAX_COMPOSER_CHARS ? value : undefined;
519
+ }
520
+ catch {
521
+ return undefined;
522
+ }
523
+ return undefined;
524
+ }
525
+ async function uniqueVisibleLocator(page, factory) {
526
+ if (typeof page.getByRole !== "function")
527
+ return undefined;
528
+ let locator;
529
+ try {
530
+ locator = factory(page);
531
+ }
532
+ catch {
533
+ return undefined;
534
+ }
535
+ if (typeof locator.count !== "function")
536
+ return undefined;
537
+ let count;
538
+ try {
539
+ count = await locator.count();
540
+ }
541
+ catch {
542
+ return undefined;
543
+ }
544
+ if (!Number.isSafeInteger(count) || count < 1 || count > MAX_LOCATOR_CANDIDATES)
545
+ return undefined;
546
+ const visible = [];
547
+ for (let index = 0; index < count; index += 1) {
548
+ const candidate = count === 1 ? locator : locator.nth?.(index);
549
+ if (candidate === undefined || typeof candidate.isVisible !== "function")
550
+ return undefined;
551
+ try {
552
+ if (await candidate.isVisible())
553
+ visible.push(candidate);
554
+ }
555
+ catch {
556
+ return undefined;
557
+ }
558
+ }
559
+ return visible.length === 1 ? visible[0] : undefined;
560
+ }
561
+ function targetForObservation(expected, state) {
562
+ void expected;
563
+ return state.target === undefined ? undefined : buildObservationTarget(state.target, state.authoritativeTabClaim);
564
+ }
565
+ function sendIdentity(state, expected) {
566
+ if (state.operationId === undefined || !isId(state.operationId) || state.requestDigest === undefined || !isDigest(state.requestDigest))
567
+ return undefined;
568
+ if (!isDigest(expected.targetBindingDigest))
569
+ return undefined;
570
+ return state.operationId;
571
+ }
572
+ function stableBaseline(snapshot) {
573
+ return snapshot.completeness === "complete"
574
+ && snapshot.userTurns.every(turn => turn.stableId !== undefined);
575
+ }
576
+ async function baselineForSnapshot(snapshot, evidenceDigest, operationId, targetBindingDigest) {
577
+ if (snapshot.completeness !== "complete" || !stableBaseline(snapshot))
578
+ return undefined;
579
+ const snapshotDigest = await safeDigestWith(evidenceDigest, "send-baseline-snapshot", {
580
+ operationId,
581
+ targetBindingDigest,
582
+ snapshotDigest: snapshot.snapshotDigest
583
+ }) ?? snapshot.snapshotDigest;
584
+ return {
585
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
586
+ snapshotDigest,
587
+ target: snapshot.target,
588
+ userTurns: snapshot.userTurns,
589
+ assistantTurns: snapshot.assistantTurns,
590
+ completeness: "complete"
591
+ };
592
+ }
593
+ function recoverPendingBlankBaseline(state) {
594
+ const target = state.target;
595
+ if (target === undefined
596
+ || target.targetLifecycle !== "new_pending"
597
+ || target.blankTaskEvidenceDigest === undefined
598
+ || !isDigest(target.blankTaskEvidenceDigest))
599
+ return undefined;
600
+ const observationTarget = buildObservationTarget(target, state.authoritativeTabClaim);
601
+ if (observationTarget === undefined)
602
+ return undefined;
603
+ const available = (value) => ({ status: "available", value });
604
+ const unavailable = () => ({ status: "unavailable", reason: "not_observed" });
605
+ const targetEvidence = {
606
+ provider: available(observationTarget.providerId),
607
+ browser: available(observationTarget.browserId),
608
+ tab: available(observationTarget.tabId),
609
+ thread: unavailable(),
610
+ conversation: unavailable(),
611
+ canonicalThreadUrl: unavailable(),
612
+ authoritativeTabClaim: observationTarget.authoritativeTabClaim === undefined
613
+ ? { status: "unavailable", reason: "not_exposed" }
614
+ : available(observationTarget.authoritativeTabClaim),
615
+ coordinationScope: observationTarget.coordinationScope
616
+ };
617
+ return {
618
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
619
+ snapshotDigest: target.blankTaskEvidenceDigest,
620
+ target: targetEvidence,
621
+ userTurns: [],
622
+ assistantTurns: [],
623
+ completeness: "complete"
624
+ };
625
+ }
626
+ function buildObservationTarget(target, authoritativeTabClaim) {
627
+ if (!isId(target.providerId)
628
+ || !isId(target.browserId)
629
+ || !isId(target.tabId)) {
630
+ return undefined;
631
+ }
632
+ const lifecycle = target.targetLifecycle ?? "fixed";
633
+ if (lifecycle !== "new_pending" && (target.conversationId === undefined
634
+ || !isId(target.conversationId)
635
+ || target.canonicalThreadUrl === undefined
636
+ || !OPAQUE_THREAD_URL_PATTERN.test(target.canonicalThreadUrl)))
637
+ return undefined;
638
+ if (lifecycle === "new_pending" && (target.newTargetAnchorDigest === undefined
639
+ || target.blankTaskEvidenceDigest === undefined
640
+ || !isDigest(target.newTargetAnchorDigest)
641
+ || !isDigest(target.blankTaskEvidenceDigest)))
642
+ return undefined;
643
+ if (target.coordinationScope === "provider" && (authoritativeTabClaim === undefined || !isId(authoritativeTabClaim)))
644
+ return undefined;
645
+ return {
646
+ providerId: target.providerId,
647
+ browserId: target.browserId,
648
+ tabId: target.tabId,
649
+ coordinationScope: target.coordinationScope,
650
+ targetLifecycle: lifecycle,
651
+ ...(authoritativeTabClaim === undefined ? {} : { authoritativeTabClaim }),
652
+ ...(target.conversationId === undefined ? {} : { expectedConversationId: target.conversationId })
653
+ };
654
+ }
655
+ function makeObservationTarget(target, state) {
656
+ return buildObservationTarget(target, state.authoritativeTabClaim);
657
+ }
658
+ async function readCollectorContext(request, page, target, evidenceDigest, state) {
659
+ if (request.submissionActionId === undefined || !isId(request.submissionActionId))
660
+ throw new ProductionPrimitiveError("submission_witness_unwired");
661
+ // The service projects the authenticated causal baseline into every
662
+ // collect attempt. Use it for this context read as well, so a later
663
+ // observation can request terminal metadata for the exact assistant turn
664
+ // even when a previous wait:false call returned to the caller.
665
+ const baseline = request.baseline;
666
+ if (baseline === undefined)
667
+ throw new ProductionPrimitiveError("target_evidence_unavailable");
668
+ const observationTarget = makeObservationTarget(target, state);
669
+ if (observationTarget === undefined)
670
+ throw new ProductionPrimitiveError("target_evidence_unavailable");
671
+ const observation = await observeBrowserPage(page, {
672
+ operationId: request.operationId,
673
+ target: observationTarget,
674
+ evidenceDigest,
675
+ responseContent: "metadata",
676
+ baseline
677
+ });
678
+ const binding = {
679
+ schemaVersion: TURN_OWNERSHIP_SCHEMA_VERSION,
680
+ operationId: request.operationId,
681
+ targetBindingDigest: request.targetBindingDigest,
682
+ target: observation.snapshot.target,
683
+ evidenceProfile: {
684
+ stableConversationId: "required",
685
+ stableUserTurnId: "required",
686
+ stableAssistantTurnId: "required",
687
+ stableBranchId: "required",
688
+ authoritativeTabClaim: target.coordinationScope === "provider" ? "required" : "unavailable"
689
+ },
690
+ replacementTabRecovery: false,
691
+ actionId: request.submissionActionId,
692
+ actionKind: request.submissionActionKind ?? "send"
693
+ };
694
+ // This cursor is only a candidate from the immediately preceding read. The
695
+ // service keeps the journal baseline/witness authoritative, and the
696
+ // collector reclassifies the next snapshot against both before using it.
697
+ // Never fabricate a cursor when the authenticated witness is absent or the
698
+ // exact delta cannot be classified.
699
+ let prior;
700
+ if (request.submissionWitness !== undefined) {
701
+ try {
702
+ prior = classifyTurnOwnership({
703
+ binding,
704
+ baseline,
705
+ snapshot: observation.snapshot,
706
+ submissionWitness: request.submissionWitness
707
+ }).cursor;
708
+ }
709
+ catch {
710
+ prior = undefined;
711
+ }
712
+ }
713
+ return {
714
+ binding,
715
+ baseline,
716
+ ...(prior === undefined ? {} : { prior })
717
+ };
718
+ }
719
+ async function observeCollector(request, page, target, context, evidenceDigest, state) {
720
+ const observationTarget = makeObservationTarget(target, state);
721
+ if (observationTarget === undefined)
722
+ throw new ProductionPrimitiveError("target_evidence_unavailable");
723
+ const result = await observeBrowserPage(page, {
724
+ operationId: request.operationId,
725
+ target: observationTarget,
726
+ evidenceDigest,
727
+ responseContent: request.responseContent,
728
+ ...(request.responseFormat === undefined ? {} : { responseFormat: request.responseFormat }),
729
+ ...(context.baseline === undefined ? {} : { baseline: context.baseline }),
730
+ ...(context.prior?.assistantTurnId === undefined ? {} : {
731
+ terminalAssistantTurnId: context.prior.assistantTurnId,
732
+ ...(request.responseContent === "include" ? { rawAssistantTurnId: context.prior.assistantTurnId } : {})
733
+ })
734
+ });
735
+ return {
736
+ schemaVersion: COLLECTOR_SCHEMA_VERSION,
737
+ snapshot: result.snapshot,
738
+ ...(result.terminal === undefined ? {} : { terminal: result.terminal })
739
+ };
740
+ }
741
+ async function sleepOutsideBrowser(milliseconds, signal) {
742
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 60_000)
743
+ throw new ProductionPrimitiveError("invalid_sleep");
744
+ if (signal.aborted)
745
+ throw new ProductionPrimitiveError("operation_cancelled");
746
+ await new Promise((resolve, reject) => {
747
+ const timer = setTimeout(() => {
748
+ signal.removeEventListener("abort", onAbort);
749
+ resolve();
750
+ }, milliseconds);
751
+ const onAbort = () => {
752
+ clearTimeout(timer);
753
+ signal.removeEventListener("abort", onAbort);
754
+ reject(new ProductionPrimitiveError("operation_cancelled"));
755
+ };
756
+ signal.addEventListener("abort", onAbort, { once: true });
757
+ });
758
+ }
759
+ async function observeControlTurn(request, page, target, evidenceDigest, state) {
760
+ const observation = await observeControlSnapshot(request.operationId, page, target, evidenceDigest, state);
761
+ if (observation === undefined)
762
+ return { status: "uncertain", reason: "unavailable" };
763
+ const assistant = observation.snapshot.assistantTurns.find(turn => turn.stableId === request.expectedAssistantTurnId);
764
+ if (assistant === undefined)
765
+ return { status: "mismatch", reason: "different_turn", evidenceDigest: observation.snapshot.snapshotDigest };
766
+ if (assistant.state === "generating") {
767
+ return { status: "generating", assistantTurnId: request.expectedAssistantTurnId, evidenceDigest: assistant.evidenceDigest };
768
+ }
769
+ return { status: "terminal", assistantTurnId: request.expectedAssistantTurnId, reason: "not_generating", evidenceDigest: assistant.evidenceDigest };
770
+ }
771
+ async function executeControlOnce(request, page, target, evidenceDigest, state) {
772
+ if (request.action !== "stop")
773
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
774
+ const locator = await uniqueVisibleLocator(page, stopGenerationButton);
775
+ if (locator === undefined || typeof locator.click !== "function")
776
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
777
+ try {
778
+ // Sole Stop activation. A rejection is not retried and is reconciled by
779
+ // the control coordinator's observation-only path.
780
+ await locator.click();
781
+ }
782
+ catch {
783
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
784
+ }
785
+ // Release the mutation actor before reading the postcondition. Returning an
786
+ // uncertain result instructs the control coordinator to reacquire the tab
787
+ // through its observation-only port; it must never make this primitive hold
788
+ // the actor across both activation and reconciliation.
789
+ return { status: "uncertain" };
790
+ }
791
+ async function observeControlPostcondition(request, page, target, evidenceDigest, state) {
792
+ if (request.action !== "stop")
793
+ return { status: "uncertain", blockerCode: "send_control_unavailable" };
794
+ const observation = await observeControlSnapshot(request.operationId, page, target, evidenceDigest, state);
795
+ if (observation === undefined)
796
+ return { status: "uncertain", blockerCode: "target_evidence_unavailable" };
797
+ const assistant = observation.snapshot.assistantTurns.find(turn => turn.stableId === request.expectedAssistantTurnId);
798
+ if (assistant === undefined)
799
+ return { status: "not_satisfied", blockerCode: "target_binding_mismatch", evidenceDigest: observation.snapshot.snapshotDigest };
800
+ if (assistant.state === "generating")
801
+ return { status: "not_satisfied", blockerCode: "send_control_unavailable", evidenceDigest: observation.snapshot.snapshotDigest };
802
+ return { status: "satisfied", assistantTurnId: request.expectedAssistantTurnId, evidenceDigest: assistant.evidenceDigest };
803
+ }
804
+ async function observeControlSnapshot(operationId, page, target, evidenceDigest, state) {
805
+ const observationTarget = makeObservationTarget(target, state);
806
+ if (observationTarget === undefined)
807
+ return undefined;
808
+ try {
809
+ return await observeBrowserPage(page, {
810
+ operationId,
811
+ target: observationTarget,
812
+ evidenceDigest,
813
+ responseContent: "metadata"
814
+ });
815
+ }
816
+ catch {
817
+ return undefined;
818
+ }
819
+ }
820
+ async function safeDigestWith(evidenceDigest, domain, material) {
821
+ try {
822
+ const value = await evidenceDigest(domain, material);
823
+ return isDigest(value) ? value : undefined;
824
+ }
825
+ catch {
826
+ return undefined;
827
+ }
828
+ }
829
+ async function digest(evidenceDigest, domain, material) {
830
+ return await safeDigestWith(evidenceDigest, domain, material);
831
+ }
832
+ function isDigest(value) {
833
+ return typeof value === "string" && DIGEST_PATTERN.test(value);
834
+ }
835
+ function isId(value) {
836
+ return typeof value === "string" && ID_PATTERN.test(value);
837
+ }
838
+ function validateOptions(options) {
839
+ if (options === null || typeof options !== "object" || Array.isArray(options) || typeof options.evidenceDigest !== "function") {
840
+ throw new ProductionPrimitiveError("invalid_options");
841
+ }
842
+ if (options.operationId !== undefined && !isId(options.operationId))
843
+ throw new ProductionPrimitiveError("invalid_options");
844
+ if (options.requestDigest !== undefined && !isDigest(options.requestDigest))
845
+ throw new ProductionPrimitiveError("invalid_options");
846
+ if (options.authoritativeTabClaim !== undefined && !isId(options.authoritativeTabClaim))
847
+ throw new ProductionPrimitiveError("invalid_options");
848
+ if (options.desiredComposerText !== undefined && typeof options.desiredComposerText !== "string")
849
+ throw new ProductionPrimitiveError("invalid_options");
850
+ if (options.composerText !== undefined && typeof options.composerText !== "string")
851
+ throw new ProductionPrimitiveError("invalid_options");
852
+ if (options.observeAttachments !== undefined && typeof options.observeAttachments !== "function")
853
+ throw new ProductionPrimitiveError("invalid_options");
854
+ if (options.target !== undefined && (options.target === null || typeof options.target !== "object" || Array.isArray(options.target)))
855
+ throw new ProductionPrimitiveError("invalid_options");
856
+ }