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,459 @@
1
+ ---
2
+ title: Transactional browser operations
3
+ date: 2026-08-16
4
+ type: reference
5
+ status: preview
6
+ ---
7
+
8
+ # Transactional browser operations (v1)
9
+
10
+ This is the canonical reference for the additive transactional operation
11
+ surface. It describes the versioned request/result contract, the TypeScript
12
+ and Python facades, and the boundary between the new operation path and the
13
+ existing browser-control primitives.
14
+
15
+ ## Status and current boundary
16
+
17
+ The v1 journal, strict wire validators, recovery state machine, browser-adapter
18
+ seams, Node backend commands, and direct Python sync/async clients are present
19
+ in the source tree. This is a preview contract, not a claim that every
20
+ provider/browser primitive is live.
21
+
22
+ In the current source, `createChatGPT()` opens the operation journal lazily.
23
+ `chatgpt.operations.inspect(handle)` is deliberately browser-free and can
24
+ inspect an authenticated local record. When no custom operation seam is
25
+ supplied, browser-touching `submit`, non-terminal `collect`, and `control` use
26
+ lazy request-local ChatGPT factories. Browser attachment happens only after
27
+ the journal has accepted the operation; recovery attaches by the authenticated
28
+ durable tab ID and never falls back to the selected tab. Missing bridge access,
29
+ target evidence, or a required provider primitive fails closed instead of
30
+ routing through the legacy command sequence. Supplying any custom adapter seam
31
+ is an all-or-nothing integration choice so a custom provider is never combined
32
+ with part of the default recovery path. The current ordinary backend capability
33
+ response still advertises no stable provider/browser/tab identity,
34
+ authoritative tab claim, fencing, or concurrent-tab guarantee.
35
+
36
+ Therefore:
37
+
38
+ - Contract, validation, journal, and browser-free inspection claims are
39
+ deterministic source/test claims.
40
+ - A live submit/collect/control claim requires a bridge-enabled default runtime
41
+ or a complete custom adapter, an authorized page, target evidence, and the
42
+ provider primitives needed by that operation, followed by the relevant live
43
+ smoke.
44
+ - A browser bridge blocker, unavailable primitive, missing target evidence, or
45
+ runtime mismatch is a structured boundary result; it is not evidence that
46
+ ChatGPT itself is unavailable.
47
+
48
+ The authenticated journal records a complete redacted ownership baseline
49
+ before each non-repeatable Send or Work-steer intent. New records retain these
50
+ in `ownershipBaselines[actionId]`; `ownershipBaseline` remains the compatibility
51
+ projection of the original Send. Only after the exact post-action user-turn
52
+ delta has been observed does the journal append the immutable keyed witness in
53
+ `submissionWitnesses[actionId]`; `submissionWitness` likewise remains the
54
+ original-Send compatibility projection. Each witness contains action/target
55
+ identities and digests, never prompt text, DOM, response text, URLs, or local
56
+ paths. A satisfied Work steer requires its exact baseline+witness pair, a clean
57
+ non-mutating `not_satisfied` steer retains its baseline but has no witness, and
58
+ an unresolved/uncertain steer blocks a later steer. Collection authenticates
59
+ and classifies against the latest satisfied causal action rather than silently
60
+ falling back to the original Send. A durable witness carries
61
+ `baselineSnapshotDigest`, the digest of its complete pre-action ownership
62
+ snapshot, alongside the post-action delta digest. The state reducer, service,
63
+ collector, and Python model require that value to equal the keyed durable
64
+ baseline; a witness cannot be replayed against a different action or baseline.
65
+ A legacy target-establishment
66
+ projection may be read without `postSendDeltaDigest`, but that record cannot
67
+ establish a new target or prove submitted ownership; every newly emitted
68
+ submission witness and target-establishment event must carry its required
69
+ causal evidence.
70
+
71
+ There is intentionally no permissive migration from a proof-less experimental
72
+ snapshot into `submitted`, `generating`, `capturing`, or `completed`. Those
73
+ phases require both keyed records for the original Send. A snapshot that only
74
+ has the compatibility projections fails closed and must be inspected or
75
+ explicitly discarded by its owner; the runtime cannot reconstruct a pre-Send
76
+ baseline or post-Send delta after the fact.
77
+
78
+ ## Identity model
79
+
80
+ There are several intentionally different identities:
81
+
82
+ | Identity | Owner and purpose | Safe to persist/echo? |
83
+ | --- | --- | --- |
84
+ | `operationId` | Caller-owned canonical UUID. It names one immutable logical operation and opts high-level helpers into this path. | Yes, as an opaque identity. Never generate a replacement when recovering. |
85
+ | `requestDigest` | Journal-keyed digest of the canonical request plus file manifest. It detects a changed retry. | Yes, as a digest; not as a substitute for the request. |
86
+ | `handle` | Fresh locator containing operation identity, digest, surface, revision, phase, mutation boundary, and optional target-binding digest. | Yes, as a versioned locator. It is not an authority token. |
87
+ | transport `requestId` | Backend connection identity used to demultiplex one NDJSON call. | Diagnostic only; it is not operation identity. |
88
+ | `backendSessionId` | Runtime/backend owner identity used by coordination and capability diagnostics. | Diagnostic only. |
89
+ | target binding | Authenticated provider/browser/tab/thread evidence bound to the journal record. | Persist only in its redacted/digest form defined by the contract. |
90
+
91
+ The caller owns `operationId` and must reuse it for a retry or recovery. A
92
+ different prompt, target, configuration, or file manifest under the same ID
93
+ does not create a second operation: the journal returns an identity mismatch.
94
+ The journal and action-intent CAS rules make a repeated same-ID submit
95
+ observation/recovery path unable to activate Send twice.
96
+
97
+ The TypeScript journal remains the runtime authority for request-digest
98
+ computation. Its digest primitive is
99
+ `HMAC-SHA-256(key, UTF8(domain + "\0" + canonicalJson(value)))`, returned with
100
+ the `hmac-sha256:` prefix. Every operation and journal domain uses the stable
101
+ `codex-chatgpt-control/.../v1` namespace in both private-source and exported
102
+ public builds; package-name rewriting must never alter persisted identity.
103
+ Canonical JSON sorts object keys lexicographically,
104
+ retains array order, normalizes negative zero, uses explicit internal markers
105
+ for `undefined`, dates, and bytes, and rejects caller objects containing those
106
+ reserved marker keys. The request projection binds schema/operation/surface,
107
+ target, keyed prompt digest plus UTF-8 length, configuration/tools, ordered
108
+ keyed file-name/content digests plus byte counts, path-free capture policy, and
109
+ behavior. `capturePolicy.outputDirectory` is intentionally excluded because a
110
+ separate transfer action binds destination authority. The privacy-safe fixed-key
111
+ golden vector at
112
+ `contracts/v1/vectors/operation-request-digest-v1.json` freezes the canonical
113
+ bytes, nested domains, and final digest. Node computes it in production; Node
114
+ and a test-only Python reference both verify the same vector so the Python
115
+ facade does not become a second runtime authority.
116
+
117
+ ## Canonical direct surface
118
+
119
+ The backend wire commands are exactly:
120
+
121
+ - `operations.submit`
122
+ - `operations.collect`
123
+ - `operations.inspect`
124
+ - `operations.control`
125
+
126
+ `operations.run` is an SDK-only composition of one `submit` followed by at
127
+ most one `collect`; it is not a fifth backend command and must not be treated
128
+ as an independently idempotent provider primitive.
129
+
130
+ ### Submit request
131
+
132
+ The request is strict JSON. Optional properties are omitted rather than sent
133
+ as `null`.
134
+
135
+ ```ts
136
+ const request = {
137
+ schemaVersion: "chatgpt.browser_control.operation_request.v1",
138
+ operationId: "123e4567-e89b-42d3-a456-426614174000", // caller-owned UUID
139
+ surface: "chat",
140
+ prompt: "Summarize the visible thread.",
141
+ target: {
142
+ type: "conversation_id",
143
+ conversationId: "conversation-id-from-the-caller"
144
+ },
145
+ configuration: {
146
+ experience: "chat",
147
+ modelVersion: "visible-version-label",
148
+ reasoning: "high",
149
+ tools: ["web_search"]
150
+ },
151
+ capture: {
152
+ responseContent: "metadata",
153
+ responseFormat: "markdown",
154
+ artifacts: "receipt_only"
155
+ }
156
+ };
157
+
158
+ // The default client captures the visible ChatGPT runtime lazily after the
159
+ // journal accepts the operation. A custom adapter configuration must provide
160
+ // the complete submit/recovery/control set; missing proof still fails closed.
161
+ const submitted = await chatgpt.operations.submit(request);
162
+ ```
163
+
164
+ `surface` is `chat` or `work`. The target is one of `{ type: "new" }`,
165
+ `{ type: "selected_tab" }`, `{ type: "tab_id", tabId }`,
166
+ `{ type: "conversation_id", conversationId }`, or
167
+ `{ type: "url", url }`. `files` contains request-only absolute host-local
168
+ paths; the operation client fingerprints and revalidates them before a file
169
+ handoff. `capture.responseContent` is `include` or `metadata`; artifact
170
+ capture is `receipt_only` or `transfer`, with an explicit request-only
171
+ `outputDirectory` required for transfer.
172
+
173
+ `capture.responseFormat` is an immutable request-identity field and accepts
174
+ only `markdown` (the default) or `text`. `markdown` is reconstructed from
175
+ semantic DOM structure for the exact owned assistant turn; `text` is the
176
+ formatter's normalized visible text. The bounded transient `innerHTML` used
177
+ for that one exact turn is formatted immediately and is never written to the
178
+ journal, durable receipt, snapshot, or report. Collection fails closed when
179
+ the requested format, exact assistant ID, provider shape, or content bounds
180
+ cannot be proven.
181
+
182
+ Once a request is created, its capture contract is copied into the
183
+ path-free `capturePolicy` field on the `operation_created` event and materialized
184
+ state. The durable shape is exactly `{ responseContent, responseFormat,
185
+ artifacts }`; `responseFormat` defaults to `markdown`, omitted capture defaults
186
+ to `include` plus `receipt_only`, and request-local `outputDirectory` is never
187
+ persisted. A restart therefore retains an `artifacts: "transfer"` obligation
188
+ even when the original destination is unavailable; recovery must not silently
189
+ rewrite it to `receipt_only`.
190
+
191
+ In the TypeScript in-process facade, `submit` returns a fresh `handle` plus a
192
+ service-native submission result. `submission.kind` is one of the explicit
193
+ submitted/already-submitted/completed-receipt/blocked/uncertain/cancelled
194
+ outcomes. The backend and Python facades convert this to the strict
195
+ `operation_submit_result.v1` envelope:
196
+
197
+ - `accepted`: submission was reconciled or was already durably submitted; it
198
+ is not completion.
199
+ - `completed`: a durable receipt is already available.
200
+ - `blocked`: the mutation boundary is known not to have crossed.
201
+ - `uncertain`: a handoff or Send may have crossed a non-repeatable boundary;
202
+ observe/recover, do not resubmit.
203
+
204
+ ### Collect and inspect
205
+
206
+ Always carry forward the freshest returned handle. `inspect` reads the
207
+ authenticated journal and does not use a browser. `collect` addresses only the
208
+ operation-owned assistant turn; it never falls back to “latest response” or
209
+ submits a prompt. A backend/Python flow is shaped like this:
210
+
211
+ ```python
212
+ from codex_chatgpt_control import ChatGPT
213
+
214
+ # backend is an already-configured BackendClient or compatible test double.
215
+ chatgpt = ChatGPT(backend=backend)
216
+ submitted = chatgpt.operations.submit(
217
+ operation_id="123e4567-e89b-42d3-a456-426614174000",
218
+ surface="chat",
219
+ prompt="Summarize the visible thread.",
220
+ target={"type": "conversation_id", "conversationId": "conversation-id-from-the-caller"},
221
+ capture={"responseContent": "metadata", "responseFormat": "markdown", "artifacts": "receipt_only"},
222
+ )
223
+
224
+ handle = submitted.handle
225
+ if submitted.status == "accepted":
226
+ inspected = chatgpt.operations.inspect(handle=handle)
227
+ collected = chatgpt.operations.collect(
228
+ handle=inspected.handle,
229
+ wait=False,
230
+ response_content="metadata",
231
+ )
232
+ print(collected.status) # completed, pending, blocked, or uncertain
233
+ ```
234
+
235
+ The versioned wire envelopes carry `operationId`, `requestDigest`, and a fresh
236
+ handle on every result. Completed results carry a durable receipt. Blocked or
237
+ uncertain results carry a redacted blocker with phase and mutation boundary.
238
+ An included raw response is a separately marked ephemeral `liveResponse`; it
239
+ is not part of durable state or a receipt.
240
+
241
+ `inspect` reports `completed`, `pending`, or `uncertain` from the durable phase.
242
+ `collect` reports `completed`, `pending`, `blocked`, or `uncertain`.
243
+ `control` reports `completed`, `blocked`, or `uncertain` and is covered below.
244
+
245
+ ### Control
246
+
247
+ `control` is operation-bound and requires a fresh generating parent handle, an
248
+ exact assistant turn ID, and a caller-owned `controlActionId`. Its action is
249
+ `stop` or `steer`; it does not accept a generic “latest” target. Control has
250
+ its own action digest but returns the parent operation identity and a fresh
251
+ parent handle. A failed or uncertain control result is never an instruction
252
+ to repeat Stop/steer automatically.
253
+
254
+ ## Recovery and retry rules
255
+
256
+ The safe recovery loop is:
257
+
258
+ 1. Persist the latest redacted handle and the caller-owned `operationId` in the
259
+ caller's own recovery record.
260
+ 2. Call `inspect` after a timeout, process restart, or lost response.
261
+ 3. If the operation is not terminal, call `collect` with that handle to observe
262
+ the owned turn. Use `responseContent: "metadata"` while polling when raw
263
+ text is not needed.
264
+ 4. For an uncertain handoff or Send, inspect the visible target/postcondition
265
+ and reconcile the durable action intent. Do not call `submit` again with a
266
+ new ID or a changed request.
267
+ 5. Use `control` only with the exact current generating handle and assistant
268
+ turn ID, after an explicit caller decision.
269
+
270
+ Handles are locators, not bearer authority. After a restart, a configured
271
+ `handleAdapterFactory` must rebuild a target-bound adapter only after the
272
+ journal authenticates the handle. If stable target evidence, page affinity,
273
+ or an authoritative provider claim cannot be proven, the result must remain a
274
+ blocker/uncertain outcome. A handle does not authorize taking over another
275
+ tab, conversation, or user's turn.
276
+
277
+ ## High-level opt-in behavior
278
+
279
+ The legacy path remains the compatibility boundary. TypeScript high-level
280
+ helpers select the transactional mapper only when a caller supplies a
281
+ canonical `operationId`; with no ID they retain the existing workflow/runner
282
+ sequence. Unsupported transactional fields are rejected before browser use,
283
+ and the result carries the supplied operation ID in success, blocker, partial,
284
+ or structured error data.
285
+
286
+ | Surface | With caller-owned `operationId` | Without it / current limitation |
287
+ | --- | --- | --- |
288
+ | `chatgpt.ask`, `askInThread`, `askWithFiles` | One transactional submit/collect composition. Target must be explicit enough for the mapper; response capture is metadata or Markdown/text. | Existing workflow sequence, unchanged compatibility behavior. |
289
+ | `askAndDownload` | Download is rejected before browser use; collect the operation and use an explicit artifact-transfer operation when that adapter is available. | Existing download workflow remains available. |
290
+ | `runner.run` | Same transactional mapper as Chat. Agent instructions use the rendered visible prompt semantics; `visible_setup_message` and `copy` are rejected because they require additional/legacy turns. Stream events remain milestones only. | Existing runner plan/sequence behavior. |
291
+ | `responses.create` (TypeScript) | `operationId` opts into the same runner mapper. `instructions` are visible-prefix text only; API-only fields remain unsupported; operation ID and handle are returned in `browser_control`. | Existing narrow Responses-shaped runner adapter. |
292
+ | `work.start` (TypeScript) | Uses `surface: "work"`, returns operation ID/handle and exact owned-turn state. Wait/read is collect-only; unsupported configuration/wait fields fail before browser use. | Existing Work command path. |
293
+ | `work.steer` (TypeScript) | Requires the generating Work handle, matching operation ID, control action ID, and exact assistant turn ID; the parent operation is never resubmitted. | Existing Work steer primitive. |
294
+ | Python `chatgpt.operations` | Direct sync/async v1 envelopes and field aliases are parity facades over the shared Node backend. Python Runner/Responses also use this facade when given `operation_id`. | Calls without an operation ID retain the legacy Python runner/Responses paths. |
295
+
296
+ The current transactional mapper intentionally does not claim support for
297
+ `visible_setup_message`, copy/download/report side effects, arbitrary API model
298
+ controls, title/history search targets, or wait semantics that are not tied to
299
+ the owned turn. Rejections are input results, not browser attempts.
300
+
301
+ ## Work, Runner, and Responses semantics
302
+
303
+ Rendered agent instructions remain visible prompt content. The transactional
304
+ runner does not turn them into hidden system messages and does not change the
305
+ legacy rendering/precedence rules for target, configuration, wait, or read
306
+ values. It does not promise token deltas: all runner streaming is milestone
307
+ streaming (`message.in_progress`, `message.completed`, and related setup or
308
+ blocker milestones).
309
+
310
+ Work start is a single operation on the Work surface. A `read` request means
311
+ “collect this operation-owned turn”, not “read whichever response is latest”.
312
+ Work steer is a separate operation-bound control action. It cannot combine
313
+ steer with a second wait/read request, and it must not be used to submit the
314
+ original Work prompt again.
315
+
316
+ The TypeScript and Python Responses adapters accept a caller-owned operation ID
317
+ (`operationId` / `operation_id`) and route it through the same one-submit,
318
+ at-most-one-collect mapper. Both preserve operation identity and the fresh
319
+ handle in their browser-control metadata; malformed or API-only combinations
320
+ are rejected before backend traffic, and an accepted operation is never
321
+ resubmitted internally.
322
+
323
+ Direct collect calls and high-level transactional waits may set a bounded
324
+ `pollIntervalMs` (`poll_interval_ms` in Python; Runner wait objects also accept
325
+ the documented poll aliases). It affects only the sleep between observation
326
+ attempts, accepts integers from `0` through `60000`, and does not hold the
327
+ browser/tab coordinator while sleeping.
328
+
329
+ ## Per-tab and shared-browser coordination
330
+
331
+ The coordinator is deliberately short-lived at the browser boundary:
332
+
333
+ - One target binding captures one explicit page and stable provider/browser/tab
334
+ evidence. Every read or mutation re-checks page/target affinity before acting.
335
+ - A short page transaction acquires a process-local tab actor when stable
336
+ provider/browser/tab identities and concurrent-tab capability are proven.
337
+ An authoritative claim/fencing token additionally upgrades that binding to
338
+ provider coordination scope; it is not fabricated as a prerequisite for
339
+ safe coordination among cooperating callers in this one process.
340
+ - If those capabilities are absent or downgraded, the resource scope is the
341
+ process-local browser actor. The implementation must not infer safe
342
+ per-tab concurrency from an unverified tab label.
343
+ - Hashing/revalidation, journal I/O, report work, polling, and sleeps occur
344
+ outside the coordinator actor. A long ChatGPT generation therefore does not
345
+ hold the tab/browser scheduler lock.
346
+ - A deadline or cancellation quarantines an in-flight callback until it
347
+ settles; callers must not start a duplicate non-repeatable action merely
348
+ because the host request returned.
349
+ - The process-local coordinator only coordinates cooperating callers in one
350
+ process. It is not a provider-level or cross-process fencing guarantee.
351
+
352
+ The public package exports `createProcessTabCoordinator`/`ProcessTabCoordinator`
353
+ and `createOperationRuntimeContext`/`OperationRuntimeContext` because the
354
+ public browser-adapter options accept injected coordinator and runtime-context
355
+ instances. Integrations should inject these only with provider evidence that
356
+ matches the negotiated identity and concurrency capabilities.
357
+
358
+ The current default backend advertises `tabs.coordinationScope: "none"` and
359
+ `tabs.concurrentTabs: false`. A future/provider-backed integration may
360
+ advertise stronger capabilities, but callers must check the negotiated
361
+ `backend.hello`/`backend.capabilities` result and retain the conservative
362
+ scope when identity or fencing is unavailable. Concurrent operations against a
363
+ shared browser are not categorically forbidden; they are allowed only where
364
+ the provider capability and target-binding evidence prove that the distinct
365
+ tab actors are safe.
366
+
367
+ ## Version and capability checks
368
+
369
+ Before relying on an operation or concurrency guarantee, negotiate the backend
370
+ identity/capabilities. Check at least:
371
+
372
+ - `protocolVersion` and `supportedProtocolVersions`;
373
+ - the four `operations.*` command names;
374
+ - package/runtime/build identity (`packageVersion`, `runtimeVersion`,
375
+ `buildDigest`), retaining `unknown` as unknown rather than guessing;
376
+ - request IDs, multiplexing, and cancellation claims; and
377
+ - `tabs` identity, coordination scope, authoritative claim/fencing, and
378
+ `concurrentTabs`.
379
+
380
+ A package/runtime/build value that is old or unknown is a warning for
381
+ diagnostics and deployment policy. It is not proof of incompatibility. A
382
+ proven protocol mismatch, unsupported required command, failed capability
383
+ intersection, or an adapter that cannot satisfy target/page affinity is a
384
+ blocker (`runtime_incompatible`, `backend_unavailable`, or the more specific
385
+ target blocker). Do not turn a version warning into a live-support claim.
386
+
387
+ ## Bounded transport and artifact I/O
388
+
389
+ Multiplexing does not mean unbounded buffering. Node and Python persistent
390
+ clients bound each stream by event count and encoded UTF-8 bytes, isolate an
391
+ overflow to that correlated route, and retain bounded tombstones for expected
392
+ late terminal records. The Node client additionally bounds aggregate queued
393
+ stdin frames by count and bytes. A queued request is re-authorized immediately
394
+ before its frame is written; cancellation before that point prevents the late
395
+ write. A started stdin write that never settles forces process teardown, and a
396
+ second unresolved write generation fails closed rather than accumulating
397
+ unbounded promise chains.
398
+
399
+ Operation artifact transfer is streaming and holds only a bounded provider
400
+ chunk plus filesystem buffers. Provider `open`, `next`, and `return` calls have
401
+ hard request/deadline boundaries and a 30-second internal fallback when the
402
+ caller supplies no shorter bound. Late settlements are observed but never
403
+ retried. One provider chunk is limited to 8 MiB and one artifact to 65,536
404
+ chunks; the built-in 64 KiB adapters need at most 8,192 chunks for the 512 MiB
405
+ artifact ceiling. This admits an immediately terminal empty artifact while an
406
+ infinite sequence of empty/tiny chunks fails closed. Local output uses a
407
+ restrictive operation-owned temporary file, hashes while writing, syncs before
408
+ exclusive installation, and never overwrites an unrelated destination.
409
+
410
+ The transactional artifact provider has a stricter capability boundary than
411
+ the legacy download helpers. Browser-backed enumeration and acquisition require
412
+ a bounded `page.evaluate` implementation plus a causal download-event waiter.
413
+ For bytes, `DownloadLike.createReadStream()` is preferred. A completed local
414
+ `path()` is accepted only when it names an absolute regular file that can be
415
+ opened with `O_NOFOLLOW`, retained by file descriptor, and proven to keep the
416
+ same file identity and bounded size. A provider exposing only `saveAs(path)` is
417
+ rejected without calling it: pure Node cannot prevent that pathname capability
418
+ from being redirected after validation. Custom adapters must implement these
419
+ operation capabilities explicitly; support for legacy `saveAs()` elsewhere in
420
+ the SDK does not imply transactional artifact-transfer support.
421
+
422
+ ## Privacy, retention, and redaction
423
+
424
+ The operation boundary is designed so durable state contains identities,
425
+ digests, counts, phases, action intents/receipts, redacted blockers, target
426
+ evidence, and artifact metadata—not raw prompts, local input paths, or raw
427
+ assistant text. Request-only paths are replaced by bounded placeholders before
428
+ the service persists a request. File contents are fingerprinted locally and
429
+ revalidated before handoff; hashes are diagnostics, not content transfer.
430
+
431
+ The local journal is authenticated and append-only in normal operation, uses a
432
+ state-root key and restricted local file permissions, and defaults to a 64 MiB
433
+ state quota. Quota admission also has fixed fail-closed scan ceilings of 65,536
434
+ directory entries and 256 MiB of observed file data. Exceeding the configured
435
+ state quota returns `journal_quota_exceeded`; exceeding either hard scan ceiling
436
+ returns `journal_scan_limit`. Neither condition deletes unresolved or terminal
437
+ deduplication evidence automatically. Completed receipts may be compacted;
438
+ explicit pruning leaves an
439
+ authenticated tombstone so expiry is distinguishable from “not found”. These
440
+ are local retention mechanisms, not a promise of indefinite history. Raw
441
+ response text, when requested, is an explicitly ephemeral value and is never a
442
+ durable receipt. Keep live reports, prompts, responses, paths, account data,
443
+ and credentials out of public fixtures and durable docs.
444
+
445
+ ## Compatibility boundary
446
+
447
+ The existing `ask`, `runner`, Work, Responses, message, attachment, download,
448
+ and report primitives are still supported where documented. The transactional
449
+ surface is additive and does not silently rewrite callers that omit
450
+ `operationId`. Legacy primitives do not acquire operation journaling,
451
+ submit-once recovery, operation handles, or cross-process concurrency merely
452
+ because the same client also exposes `chatgpt.operations`.
453
+
454
+ Use the direct v1 surface when the caller needs durable identity, exact target
455
+ ownership, inspect/collect recovery, or an auditable mutation boundary. Use
456
+ the legacy surface when its broader visible workflow semantics are required
457
+ and the caller accepts the legacy retry/timeout boundary. Do not mix the two
458
+ paths for one logical Send unless the caller has an explicit, reviewed bridge
459
+ between their identities.
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: Journal service for restricted browser hosts
3
+ date: 2026-09-06
4
+ type: runbook
5
+ status: implemented
6
+ ---
7
+
8
+ # Journal service for restricted browser hosts
9
+
10
+ Transactional workflows need a real Node process to own journal files and locks.
11
+ Some browser hosts expose filesystem access but no process identity or local
12
+ network sockets. Configure the explicit private-file journal service in these
13
+ hosts. Browser operations and prepared Send capabilities remain in the browser
14
+ host; only journal storage and signing use the service.
15
+
16
+ This private-file transport supports macOS and Linux. Windows is explicitly
17
+ rejected with `journal_rpc_unsupported_platform` before filesystem access;
18
+ POSIX mode bits do not establish Windows ownership or ACL protection. This
19
+ limit applies to the new service transport. The existing local Node journal
20
+ path and ordinary Node/Python Windows support retain their current behavior.
21
+
22
+ Start the packaged service in an ordinary Node terminal. Supply a persistent,
23
+ absolute state root and a **new** absolute session directory. On macOS use the
24
+ canonical `/private/tmp` spelling when choosing a temporary directory.
25
+
26
+ ```bash
27
+ node /absolute/plugin/runtime/node/codex-chatgpt-control-journal.mjs \
28
+ --state-root /absolute/private/journal-state \
29
+ --directory /absolute/private/new-journal-session
30
+ ```
31
+
32
+ For a source checkout, build with `npm run bundle:journal` and use
33
+ `dist/codex-chatgpt-control-journal.mjs`. The service prints only a readiness
34
+ result. Keep `new-journal-session/connection.json` private: it contains the
35
+ session authentication secret. Never include its contents in a prompt, report,
36
+ issue, or log.
37
+
38
+ After initializing the supported Chrome bridge, create the browser client:
39
+
40
+ ```js
41
+ const chatgpt = createChatGPT({
42
+ agent,
43
+ operations: {
44
+ journalService: {
45
+ descriptorPath: "/absolute/private/new-journal-session/connection.json"
46
+ }
47
+ }
48
+ });
49
+ const operationId = crypto.randomUUID();
50
+ const submitted = await chatgpt.ask({
51
+ operationId,
52
+ prompt: "Reply with exactly JOURNAL_OK",
53
+ experience: "chat",
54
+ configuration: { effort: "High" },
55
+ files: [],
56
+ wait: false
57
+ });
58
+ ```
59
+
60
+ Do not set `operations.stateRoot` together with `journalService`: the service
61
+ owns the state root. Without `journalService`, the existing local Node journal
62
+ path remains unchanged. The client never starts a hidden process or falls back
63
+ to another transport. `timeoutMs` on the connection bounds each journal call;
64
+ it is not a deadline for the complete browser workflow.
65
+
66
+ The service uses owner-only directories and files, authenticated encrypted
67
+ request/response envelopes, bounded request admission, and replay checks. Journal
68
+ signing keys remain in the service. It uses the real Node PID and existing lock
69
+ liveness checks. The connection secret grants journal authority to its holder;
70
+ this is a same-user local service, not a boundary against a hostile process
71
+ running as that same user.
72
+
73
+ ## Recovery
74
+
75
+ Stop the owned service with Ctrl-C or SIGTERM. To restart, retain the state root
76
+ and choose another new session directory, then create a new browser client with
77
+ that descriptor. Existing operation IDs, handles and completed receipts remain
78
+ valid. Retry an unconfirmed Send with the **same operation ID and unchanged
79
+ request**. After authenticating that request and its durable Send action, a fresh
80
+ client can reconcile the saved `new_pending` target through an observe-only
81
+ path. It requires the exact saved provider/browser/tab, its blank-task anchor,
82
+ a complete persisted pre-Send baseline, and exactly one new user turn whose
83
+ normalized text matches the requested prompt. The service then records the
84
+ proved conversation and collects its response without another Send.
85
+
86
+ This path never resolves `target: new` again, switches surfaces, restores
87
+ configuration, uploads files, or recomposes the prompt. A saved pending target
88
+ without a durable Send is blocked before browser access. Generic `collect` and
89
+ control calls still reject `new_pending` targets; use same-request submit/ask
90
+ reconciliation first. Missing tabs, unavailable baselines, different prompts,
91
+ and multiple new user turns remain blockers. Fixed or established targets also
92
+ require their exact saved conversation identity. Completed receipts replay
93
+ without browser access.
94
+
95
+ An exact saved tab and matching prompt do not provide provider navigation
96
+ history. Manual navigation on that tab to another conversation containing the
97
+ identical single prompt is observationally indistinguishable from the intended
98
+ Send. Do not navigate or repurpose the operation's tab while recovering it.
99
+
100
+ A missing or invalid connection blocks the workflow. A dispatched journal write
101
+ whose acknowledgement is lost returns `journal_rpc_outcome_indeterminate`.
102
+ Reconnect to the same journal and inspect or reconcile the **same operation
103
+ identity** before further browser actions. Never mint a new operation ID to
104
+ retry an uncertain Send. The client does not retry transport writes.
105
+
106
+ Transactional Chat Power selection observes the closed composer's effort,
107
+ records one action intent, and then searches the owned slider one step at a
108
+ time within a finite budget. An absent choice is restored only while every
109
+ step and the original position remain proven in the same execution. An
110
+ interrupted action is reconciled by observation; it never restarts probing or
111
+ reconstructs a setting from a digest.
112
+
113
+ ## Qualification and Python
114
+
115
+ The optional `transactional-submit-once` smoke uses the production client. Set
116
+ `CHATGPT_E2E_TRANSACTIONAL=1` and `CHATGPT_E2E_JOURNAL_DESCRIPTOR` in its explicit
117
+ context environment to select this service. It checks the exact response,
118
+ receipt and browser turn counts before a same-ID replay.
119
+
120
+ Fresh-client restart qualification also exercised a real completed ChatGPT
121
+ exchange against an isolated authenticated journal prefix ending at the atomic
122
+ `action_prepared` Send event: phase `ready`, target `new_pending`, persisted
123
+ baseline, and no submission witness. After restarting the normal Node journal
124
+ service, a fresh browser client recovered the exact 26-byte synthetic response,
125
+ retained the same Send action, and preserved one user/one assistant turn before
126
+ and after recovery and completed replay. This tests loss of post-Send
127
+ bookkeeping against a real browser result; it does not claim an OS process was
128
+ killed at every individual timing boundary.
129
+
130
+ Python continues to use the versioned Node backend protocol. Configure
131
+ `BackendSession({ agent, operations: { journalService: { descriptorPath } } })`
132
+ in the active browser host; Python handles, operation results and wire schemas
133
+ are unchanged. Python cannot acquire the browser bridge by spawning a plain
134
+ Node subprocess. A bridge-hosted backend transport is still required, and the
135
+ HTTP test relay is usable only where that host permits local sockets.
136
+
137
+ The async journal authority and async DOM factory helpers are TypeScript host
138
+ integration APIs, not new Python wire methods. Owner: SDK runtime maintainers.
139
+ Revisit this intentional language asymmetry if Python gains its own browser
140
+ backend; require existing shared fixtures and conformance before adding one.
@@ -30,6 +30,40 @@ const result = await chatgpt.runner.run(reviewer, {
30
30
 
31
31
  `runner.run()` returns a `ChatGPTRunResult` with `output_text`, `finalOutput`, `output`, `interruptions`, and `state`. Browser-control blockers are surfaced as resumable interruptions when the underlying command can be retried after user approval, login, or permission repair.
32
32
 
33
+ ### Transactional runner opt-in
34
+
35
+ The runner remains on its existing workflow/sequence path unless the caller
36
+ supplies a canonical, caller-owned `operationId`:
37
+
38
+ ```ts
39
+ const result = await chatgpt.runner.run(reviewer, {
40
+ operationId: "123e4567-e89b-42d3-a456-426614174000",
41
+ input: "Review this design.",
42
+ thread: { type: "conversationId", conversationId: "conversation-id-from-the-caller" },
43
+ response: { format: "markdown" }
44
+ });
45
+
46
+ // Transactional results carry the same operation identity and locator in data.
47
+ console.log(result.data?.operationId, result.data?.handle);
48
+ ```
49
+
50
+ With an operation ID, the runner reuses the transactional Chat mapper: the
51
+ rendered agent instructions remain visible prompt text, target/configuration
52
+ precedence is preserved, and the caller receives operation identity in success,
53
+ partial, blocker, and structured error data. The transactional path rejects
54
+ `visible_setup_message` (it would require a separate setup turn) and `copy`
55
+ before browser use. Downloads/reports and unsupported wait/read fields remain
56
+ explicitly rejected rather than being silently emulated. Omit the ID to retain
57
+ legacy behavior byte-for-behavior.
58
+
59
+ With no custom operation seam, `createChatGPT()` installs lazy request-local
60
+ ChatGPT adapter factories. They still fail closed when the bridge, target
61
+ evidence, or required provider primitive is unavailable, and their presence is
62
+ not a live-capability claim. For durable recovery, persist the returned handle
63
+ and use inspect/collect on the same operation; never retry the runner with a new
64
+ ID after an uncertain Send. See [Transactional browser operations](2026-08-16-transactional-operations.md)
65
+ for the direct v1 surface and capability boundary.
66
+
33
67
  `experience` and `configuration` are visible product preferences. When present,
34
68
  the plan emits `experience.open` and strict `configuration.apply` steps before
35
69
  the prompt. Successful results expose `experience.opened` and