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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +27 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +40 -1
  4. package/contracts/v1/fixtures/backend-compatibility.json +21 -0
  5. package/contracts/v1/fixtures/backend-version.json +6 -1
  6. package/contracts/v1/fixtures/operation-action-prepared-event.json +42 -0
  7. package/contracts/v1/fixtures/operation-action.json +13 -0
  8. package/contracts/v1/fixtures/operation-artifact-receipt.json +14 -0
  9. package/contracts/v1/fixtures/operation-artifact-transfer-intent-event.json +24 -0
  10. package/contracts/v1/fixtures/operation-artifact-transfer-receipt-event.json +26 -0
  11. package/contracts/v1/fixtures/operation-artifact-transfer-state.json +178 -0
  12. package/contracts/v1/fixtures/operation-blocker.json +10 -0
  13. package/contracts/v1/fixtures/operation-collect-request.json +17 -0
  14. package/contracts/v1/fixtures/operation-collect-result.json +42 -0
  15. package/contracts/v1/fixtures/operation-control-receipt.json +13 -0
  16. package/contracts/v1/fixtures/operation-control-request.json +18 -0
  17. package/contracts/v1/fixtures/operation-control-result.json +31 -0
  18. package/contracts/v1/fixtures/operation-event.json +18 -0
  19. package/contracts/v1/fixtures/operation-handle.json +10 -0
  20. package/contracts/v1/fixtures/operation-inspect-request.json +13 -0
  21. package/contracts/v1/fixtures/operation-inspect-result.json +205 -0
  22. package/contracts/v1/fixtures/operation-ownership-baseline-event.json +34 -0
  23. package/contracts/v1/fixtures/operation-receipt.json +32 -0
  24. package/contracts/v1/fixtures/operation-recovery-decision.json +7 -0
  25. package/contracts/v1/fixtures/operation-recovery-observation.json +12 -0
  26. package/contracts/v1/fixtures/operation-request.json +33 -0
  27. package/contracts/v1/fixtures/operation-state.json +144 -0
  28. package/contracts/v1/fixtures/operation-submission-witness-event.json +20 -0
  29. package/contracts/v1/fixtures/operation-submission-witness.json +11 -0
  30. package/contracts/v1/fixtures/operation-submit-result.json +16 -0
  31. package/contracts/v1/fixtures/operation-target-established-event.json +21 -0
  32. package/contracts/v1/manifest.json +155 -1
  33. package/contracts/v1/parity-suite.json +96 -1
  34. package/contracts/v1/schemas/backend-compatibility.schema.json +65 -0
  35. package/contracts/v1/schemas/backend-request.schema.json +8 -2
  36. package/contracts/v1/schemas/capabilities.schema.json +81 -1
  37. package/contracts/v1/schemas/manifest.schema.json +57 -0
  38. package/contracts/v1/schemas/operation-action.schema.json +155 -0
  39. package/contracts/v1/schemas/operation-artifact-receipt.schema.json +89 -0
  40. package/contracts/v1/schemas/operation-blocker.schema.json +70 -0
  41. package/contracts/v1/schemas/operation-collect-request.schema.json +46 -0
  42. package/contracts/v1/schemas/operation-collect-result.schema.json +74 -0
  43. package/contracts/v1/schemas/operation-control-receipt.schema.json +108 -0
  44. package/contracts/v1/schemas/operation-control-request.schema.json +111 -0
  45. package/contracts/v1/schemas/operation-control-result.schema.json +112 -0
  46. package/contracts/v1/schemas/operation-event.schema.json +756 -0
  47. package/contracts/v1/schemas/operation-handle.schema.json +55 -0
  48. package/contracts/v1/schemas/operation-inspect-request.schema.json +42 -0
  49. package/contracts/v1/schemas/operation-inspect-result.schema.json +1662 -0
  50. package/contracts/v1/schemas/operation-receipt.schema.json +153 -0
  51. package/contracts/v1/schemas/operation-recovery.schema.json +314 -0
  52. package/contracts/v1/schemas/operation-request.schema.json +158 -0
  53. package/contracts/v1/schemas/operation-state.schema.json +691 -0
  54. package/contracts/v1/schemas/operation-submission-witness.schema.json +48 -0
  55. package/contracts/v1/schemas/operation-submit-result.schema.json +94 -0
  56. package/contracts/v1/surface-drift-policy.json +50 -0
  57. package/contracts/v1/vectors/operation-request-digest-v1.json +43 -0
  58. package/dist/codex-chatgpt-control-backend.mjs +43897 -9094
  59. package/dist/codex-chatgpt-control.bundle.mjs +46360 -10262
  60. package/dist/src/backend/client.d.ts +103 -2
  61. package/dist/src/backend/client.js +1436 -97
  62. package/dist/src/backend/compatibility.d.ts +12 -0
  63. package/dist/src/backend/compatibility.js +208 -0
  64. package/dist/src/backend/protocol.d.ts +84 -1
  65. package/dist/src/backend/protocol.js +31 -5
  66. package/dist/src/backend/runtime-identity.d.ts +10 -0
  67. package/dist/src/backend/runtime-identity.js +141 -0
  68. package/dist/src/backend/session.d.ts +10 -2
  69. package/dist/src/backend/session.js +478 -7
  70. package/dist/src/backend/stdio-server.d.ts +4 -1
  71. package/dist/src/backend/stdio-server.js +240 -28
  72. package/dist/src/browser/active-composer-file-input.d.ts +9 -0
  73. package/dist/src/browser/active-composer-file-input.js +33 -0
  74. package/dist/src/browser/attach.d.ts +4 -1
  75. package/dist/src/browser/attach.js +136 -28
  76. package/dist/src/browser/downloads.d.ts +2 -0
  77. package/dist/src/client.d.ts +38 -0
  78. package/dist/src/client.js +1222 -59
  79. package/dist/src/commands/configuration.js +13 -7
  80. package/dist/src/commands/doctor.d.ts +1 -1
  81. package/dist/src/commands/doctor.js +26 -3
  82. package/dist/src/commands/experience.js +35 -0
  83. package/dist/src/commands/files.js +4 -27
  84. package/dist/src/commands/modes.js +49 -17
  85. package/dist/src/commands/power-discovery.d.ts +130 -0
  86. package/dist/src/commands/power-discovery.js +627 -0
  87. package/dist/src/commands/project-sources.js +325 -31
  88. package/dist/src/commands/sequence.js +59 -32
  89. package/dist/src/commands/session.js +7 -2
  90. package/dist/src/commands/work.js +31 -0
  91. package/dist/src/errors.d.ts +9 -0
  92. package/dist/src/errors.js +22 -0
  93. package/dist/src/index.d.ts +6 -0
  94. package/dist/src/index.js +9 -0
  95. package/dist/src/operations/artifact-output.d.ts +69 -0
  96. package/dist/src/operations/artifact-output.js +1748 -0
  97. package/dist/src/operations/artifact-stream.d.ts +19 -0
  98. package/dist/src/operations/artifact-stream.js +35 -0
  99. package/dist/src/operations/artifact-transfer.d.ts +149 -0
  100. package/dist/src/operations/artifact-transfer.js +1312 -0
  101. package/dist/src/operations/browser-adapter.d.ts +161 -0
  102. package/dist/src/operations/browser-adapter.js +2023 -0
  103. package/dist/src/operations/browser-observation.d.ts +110 -0
  104. package/dist/src/operations/browser-observation.js +1208 -0
  105. package/dist/src/operations/browser-target.d.ts +103 -0
  106. package/dist/src/operations/browser-target.js +489 -0
  107. package/dist/src/operations/canonical.d.ts +4 -0
  108. package/dist/src/operations/canonical.js +412 -0
  109. package/dist/src/operations/chatgpt-runtime.d.ts +72 -0
  110. package/dist/src/operations/chatgpt-runtime.js +1150 -0
  111. package/dist/src/operations/client.d.ts +138 -0
  112. package/dist/src/operations/client.js +1143 -0
  113. package/dist/src/operations/collector.d.ts +195 -0
  114. package/dist/src/operations/collector.js +1039 -0
  115. package/dist/src/operations/control.d.ts +372 -0
  116. package/dist/src/operations/control.js +1498 -0
  117. package/dist/src/operations/file-identity.d.ts +33 -0
  118. package/dist/src/operations/file-identity.js +153 -0
  119. package/dist/src/operations/handle.d.ts +19 -0
  120. package/dist/src/operations/handle.js +727 -0
  121. package/dist/src/operations/index.d.ts +22 -0
  122. package/dist/src/operations/index.js +22 -0
  123. package/dist/src/operations/journal.d.ts +142 -0
  124. package/dist/src/operations/journal.js +1931 -0
  125. package/dist/src/operations/production-attachments.d.ts +116 -0
  126. package/dist/src/operations/production-attachments.js +1018 -0
  127. package/dist/src/operations/production-chatgpt-artifacts.d.ts +74 -0
  128. package/dist/src/operations/production-chatgpt-artifacts.js +1146 -0
  129. package/dist/src/operations/production-chatgpt-attachments.d.ts +71 -0
  130. package/dist/src/operations/production-chatgpt-attachments.js +1563 -0
  131. package/dist/src/operations/production-configuration.d.ts +42 -0
  132. package/dist/src/operations/production-configuration.js +1363 -0
  133. package/dist/src/operations/production-primitives.d.ts +53 -0
  134. package/dist/src/operations/production-primitives.js +1073 -0
  135. package/dist/src/operations/production-work-steer.d.ts +203 -0
  136. package/dist/src/operations/production-work-steer.js +1218 -0
  137. package/dist/src/operations/recovery.d.ts +80 -0
  138. package/dist/src/operations/recovery.js +174 -0
  139. package/dist/src/operations/runtime-adapter.d.ts +118 -0
  140. package/dist/src/operations/runtime-adapter.js +786 -0
  141. package/dist/src/operations/send-once.d.ts +224 -0
  142. package/dist/src/operations/send-once.js +1081 -0
  143. package/dist/src/operations/service.d.ts +281 -0
  144. package/dist/src/operations/service.js +2810 -0
  145. package/dist/src/operations/staging.d.ts +136 -0
  146. package/dist/src/operations/staging.js +630 -0
  147. package/dist/src/operations/state-machine.d.ts +29 -0
  148. package/dist/src/operations/state-machine.js +2047 -0
  149. package/dist/src/operations/submission.d.ts +423 -0
  150. package/dist/src/operations/submission.js +1676 -0
  151. package/dist/src/operations/turn-ownership.d.ts +202 -0
  152. package/dist/src/operations/turn-ownership.js +700 -0
  153. package/dist/src/operations/types.d.ts +454 -0
  154. package/dist/src/operations/types.js +20 -0
  155. package/dist/src/operations/wire-requests.d.ts +21 -0
  156. package/dist/src/operations/wire-requests.js +396 -0
  157. package/dist/src/operations/wire-results.d.ts +97 -0
  158. package/dist/src/operations/wire-results.js +818 -0
  159. package/dist/src/runner/responses.d.ts +3 -1
  160. package/dist/src/runner/responses.js +19 -1
  161. package/dist/src/runner/result.js +227 -55
  162. package/dist/src/runner/types.d.ts +12 -0
  163. package/dist/src/runtime/command-routing.d.ts +149 -0
  164. package/dist/src/runtime/command-routing.js +431 -0
  165. package/dist/src/runtime/coordinated-browser.d.ts +24 -0
  166. package/dist/src/runtime/coordinated-browser.js +315 -0
  167. package/dist/src/runtime/coordinated-page.d.ts +39 -0
  168. package/dist/src/runtime/coordinated-page.js +634 -0
  169. package/dist/src/runtime/operation-context.d.ts +142 -0
  170. package/dist/src/runtime/operation-context.js +410 -0
  171. package/dist/src/runtime/runtime-session.d.ts +95 -0
  172. package/dist/src/runtime/runtime-session.js +314 -0
  173. package/dist/src/runtime/tab-coordinator.d.ts +200 -0
  174. package/dist/src/runtime/tab-coordinator.js +1200 -0
  175. package/dist/src/runtime/value-boundaries.d.ts +18 -0
  176. package/dist/src/runtime/value-boundaries.js +49 -0
  177. package/dist/src/safety/untrusted-output.js +2 -1
  178. package/dist/src/scripts/backend-server.js +4 -1
  179. package/dist/src/scripts/live-smoke/harness.js +79 -7
  180. package/dist/src/scripts/live-smoke/scenarios.d.ts +28 -0
  181. package/dist/src/scripts/live-smoke/scenarios.js +110 -18
  182. package/dist/src/scripts/live-smoke/types.d.ts +3 -0
  183. package/dist/src/scripts/release-canary-module.js +17 -4
  184. package/dist/src/types.d.ts +38 -1
  185. package/package.json +1 -1
  186. package/references/2026-08-16-transactional-operations.md +459 -0
  187. package/references/agents-runner.md +34 -0
  188. package/references/backend-protocol.md +73 -0
  189. package/references/python-parity.md +72 -0
  190. package/references/responses-adapter.md +19 -0
  191. package/references/streaming.md +6 -0
@@ -3,6 +3,8 @@ import type { BootstrapArgs, ChatGPTExperience, ConfigurationSelection, Response
3
3
  import type { ChatGPTAttachmentInput, ChatGPTInputItem, ChatGPTResponse, ChatGPTRunInput, ChatGPTRunResult, ChatGPTThreadSelector, ChatGPTVisibleModePreference, ChatGPTVisibleToolPreference, UnsupportedField } from "./types.js";
4
4
  export type ChatGPTResponsesCreateArgs = {
5
5
  input: string | ChatGPTInputItem[];
6
+ /** Caller-owned durable identity; opts this invocation into operations.run. */
7
+ operationId?: string;
6
8
  thread?: ChatGPTThreadSelector;
7
9
  existingTab?: BootstrapArgs["existingTab"];
8
10
  preferExistingTab?: boolean;
@@ -29,4 +31,4 @@ export type ResponsesValidationResult = {
29
31
  export declare function validateResponsesCreateArgs(args: Record<string, unknown>): ResponsesValidationResult;
30
32
  export declare function responsesCreateArgsToRunInput(args: ChatGPTResponsesCreateArgs): ChatGPTRunInput;
31
33
  export declare function responseFromRunResult<TOutput>(result: ChatGPTRunResult<TOutput>, now?: Date): ChatGPTResponse;
32
- export declare function unsupportedResponse(unsupported: UnsupportedField[], now?: Date): ChatGPTResponse;
34
+ export declare function unsupportedResponse(unsupported: UnsupportedField[], now?: Date, operationId?: string): ChatGPTResponse;
@@ -1,6 +1,7 @@
1
1
  import { renderUntrustedOutputReturnEnvelope } from "../safety/untrusted-output.js";
2
2
  const acceptedTopLevelFields = new Set([
3
3
  "input",
4
+ "operationId",
4
5
  "thread",
5
6
  "existingTab",
6
7
  "preferExistingTab",
@@ -38,6 +39,7 @@ const responseFormats = new Set([
38
39
  "blocks",
39
40
  "all"
40
41
  ]);
42
+ const operationIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
41
43
  export function validateResponsesCreateArgs(args) {
42
44
  const unsupported = [];
43
45
  for (const [path, alternative] of Object.entries(unsupportedAlternatives)) {
@@ -61,6 +63,13 @@ export function validateResponsesCreateArgs(args) {
61
63
  alternative: "Provide input: \"your visible prompt\"."
62
64
  });
63
65
  }
66
+ if (args.operationId !== undefined && (typeof args.operationId !== "string" || !operationIdPattern.test(args.operationId))) {
67
+ unsupported.push({
68
+ path: "operationId",
69
+ reason: "operationId must be a canonical UUID when provided.",
70
+ alternative: "Provide a caller-owned UUID, or omit operationId to retain the legacy runner path."
71
+ });
72
+ }
64
73
  if (args.stream !== undefined && args.stream !== false) {
65
74
  unsupported.push({
66
75
  path: "stream",
@@ -108,6 +117,8 @@ export function responsesCreateArgsToRunInput(args) {
108
117
  input: args.input,
109
118
  response: { format: args.text?.format ?? "markdown" }
110
119
  };
120
+ if (args.operationId !== undefined)
121
+ runInput.operationId = args.operationId;
111
122
  if (args.thread !== undefined)
112
123
  runInput.thread = args.thread;
113
124
  if (args.existingTab !== undefined)
@@ -148,6 +159,12 @@ export function responseFromRunResult(result, now = new Date()) {
148
159
  browserControl.completionState = completionState;
149
160
  if (generationActive !== undefined)
150
161
  browserControl.generationActive = generationActive;
162
+ const operationId = result.data?.operationId;
163
+ if (operationId !== undefined)
164
+ browserControl.operationId = operationId;
165
+ const handle = result.data?.handle;
166
+ if (handle !== undefined)
167
+ browserControl.handle = handle;
151
168
  if (result.output_text.length > 0) {
152
169
  const envelopeArgs = {
153
170
  outputText: result.output_text,
@@ -173,7 +190,7 @@ export function responseFromRunResult(result, now = new Date()) {
173
190
  browser_control: browserControl
174
191
  };
175
192
  }
176
- export function unsupportedResponse(unsupported, now = new Date()) {
193
+ export function unsupportedResponse(unsupported, now = new Date(), operationId) {
177
194
  return {
178
195
  id: responseId(now),
179
196
  object: "chatgpt.browser.response",
@@ -184,6 +201,7 @@ export function unsupportedResponse(unsupported, now = new Date()) {
184
201
  browser_control: {
185
202
  visibleUi: true,
186
203
  resultStatus: "unsupported",
204
+ ...(operationId === undefined ? {} : { operationId }),
187
205
  unsupported
188
206
  }
189
207
  };
@@ -1,14 +1,26 @@
1
1
  import { renderUntrustedOutputReturnEnvelope } from "../safety/untrusted-output.js";
2
2
  import { interruptionFromCommandResult } from "./interruptions.js";
3
3
  import { augmentCommandBlocker } from "./resume.js";
4
+ const MAX_RESULT_TRAVERSAL_DEPTH = 16;
5
+ const MAX_RESULT_TRAVERSAL_NODES = 2_048;
4
6
  export function toRunResult(agent, result) {
5
- const outputText = extractOutputText(result.data);
7
+ const extractedOutput = extractOutput(result.data);
8
+ const outputText = extractedOutput?.text ?? "";
6
9
  const finalOutput = parseFinalOutput(agent, outputText);
7
10
  const interruption = interruptionFromCommandResult(result, failedCommand(result));
8
11
  const interruptions = interruption === undefined ? [] : [interruption];
9
- const output = runItemsFromResult(result, outputText);
12
+ const output = runItemsFromResult(result, outputText, extractedOutput?.source);
10
13
  const state = runStateFromResult(result, interruptions);
11
14
  const data = { outputText };
15
+ const operationId = readOperationId(result.data);
16
+ const handle = readOperationHandle(result.data);
17
+ const requestDigest = readRequestDigest(result.data);
18
+ if (operationId !== undefined)
19
+ data.operationId = operationId;
20
+ if (handle !== undefined)
21
+ data.handle = handle;
22
+ if (requestDigest !== undefined)
23
+ data.requestDigest = requestDigest;
12
24
  const submissionState = readSubmissionState(result.data);
13
25
  const completionState = readCompletionState(result.data);
14
26
  const generationActive = readGenerationActive(result.data);
@@ -54,19 +66,25 @@ export function toRunResult(agent, result) {
54
66
  mapped.finalOutput = finalOutput;
55
67
  return mapped;
56
68
  }
57
- function extractOutputText(data) {
58
- if (!isRecord(data))
59
- return "";
60
- if (typeof data.responseText === "string")
61
- return data.responseText;
62
- if (typeof data.text === "string")
63
- return data.text;
64
- for (const value of Object.values(data)) {
65
- const nested = extractOutputText(value);
66
- if (nested.length > 0)
69
+ function extractOutput(data, seen = new WeakSet(), depth = 0, budget = { remaining: MAX_RESULT_TRAVERSAL_NODES }) {
70
+ if (!enterResultRecord(data, seen, depth, budget))
71
+ return undefined;
72
+ const responseText = ownDataProperty(data, "responseText");
73
+ if (typeof responseText === "string") {
74
+ return { text: responseText, source: data };
75
+ }
76
+ const text = ownDataProperty(data, "text");
77
+ if (typeof text === "string") {
78
+ return { text, source: data };
79
+ }
80
+ for (const value of ownDataValues(data)) {
81
+ const nested = extractOutput(value, seen, depth + 1, budget);
82
+ // Keep the pre-existing extractor's behavior: an empty nested record is
83
+ // not a usable result, so continue searching siblings for real output.
84
+ if (nested !== undefined && nested.text.length > 0)
67
85
  return nested;
68
86
  }
69
- return "";
87
+ return undefined;
70
88
  }
71
89
  function parseFinalOutput(agent, outputText) {
72
90
  if (outputText.length === 0)
@@ -81,16 +99,16 @@ function parseFinalOutput(agent, outputText) {
81
99
  }
82
100
  return outputText;
83
101
  }
84
- function runItemsFromResult(result, outputText) {
102
+ function runItemsFromResult(result, outputText, outputSource) {
103
+ const responseFormat = responseFormatForOutput(result.data, outputSource);
85
104
  const items = lifecycleItemsFromSteps(result.steps);
105
+ // Prompt provenance is independent from assistant-output provenance: a
106
+ // submitted prompt may be recorded in a sibling branch of the result.
86
107
  items.push(...messageItemsFromData(result.data));
87
108
  if (!items.some(item => item.type === "message.completed" || item.type === "message.in_progress") && outputText.length > 0) {
88
- if (result.status === "partial" && readCompletionState(result.data) !== "complete") {
89
- items.push(inProgressItem(outputText, readCompletionState(result.data), readGenerationActive(result.data)));
90
- }
91
- else {
92
- items.push({ type: "message.completed", role: "assistant", output_text: outputText, format: "markdown" });
93
- }
109
+ const assistant = assistantItemFromOutput(outputSource, outputText, responseFormat, result.status);
110
+ if (assistant !== undefined)
111
+ items.push(assistant);
94
112
  }
95
113
  if (result.blocker !== undefined) {
96
114
  items.push({ type: "run.blocked", blocker: augmentCommandBlocker(result.blocker) });
@@ -132,34 +150,55 @@ function lifecycleItemsFromSteps(steps) {
132
150
  }
133
151
  return items;
134
152
  }
135
- function messageItemsFromData(data) {
136
- if (!isRecord(data))
153
+ function messageItemsFromData(data, seen = new WeakSet(), depth = 0, budget = { remaining: MAX_RESULT_TRAVERSAL_NODES }) {
154
+ if (!enterResultRecord(data, seen, depth, budget))
137
155
  return [];
138
156
  const items = [];
139
- if (typeof data.prompt === "string" && data.prompt.length > 0) {
157
+ const prompt = ownDataProperty(data, "prompt");
158
+ if (typeof prompt === "string" && prompt.length > 0) {
140
159
  items.push({
141
160
  type: "message.submitted",
142
161
  role: "user",
143
- preview: data.prompt.length > 160 ? `${data.prompt.slice(0, 159)}...` : data.prompt,
162
+ preview: prompt.length > 160 ? `${prompt.slice(0, 159)}...` : prompt,
144
163
  redacted: true
145
164
  });
146
- }
147
- if (typeof data.responseText === "string" && data.responseText.length > 0) {
148
- if (readCompletionState(data) === "complete" || data.complete === true) {
149
- items.push({ type: "message.completed", role: "assistant", output_text: data.responseText, format: "markdown" });
150
- }
151
- else {
152
- items.push(inProgressItem(data.responseText, readCompletionState(data), readGenerationActive(data)));
153
- }
154
- }
155
- if (items.length > 0)
156
165
  return items;
157
- for (const value of Object.values(data)) {
158
- const nested = messageItemsFromData(value);
166
+ }
167
+ for (const value of ownDataValues(data)) {
168
+ const nested = messageItemsFromData(value, seen, depth + 1, budget);
159
169
  if (nested.length > 0)
160
170
  return nested;
161
171
  }
162
- return [];
172
+ return items;
173
+ }
174
+ function assistantItemFromOutput(outputSource, outputText, responseFormat, resultStatus) {
175
+ if (outputSource === undefined || outputText.length === 0)
176
+ return undefined;
177
+ // The selected output record is the sole authority for assistant text and
178
+ // lifecycle metadata. In particular, do not recursively borrow completion
179
+ // state from a later sibling branch.
180
+ const completionState = completionStateFromRecord(outputSource);
181
+ const complete = ownDataProperty(outputSource, "complete");
182
+ if (completionState === "complete" || complete === true) {
183
+ return { type: "message.completed", role: "assistant", output_text: outputText, format: responseFormat };
184
+ }
185
+ const incomplete = complete === false
186
+ || completionState !== undefined
187
+ || resultStatus === "partial";
188
+ if (incomplete) {
189
+ return inProgressItem(outputText, completionState, generationActiveFromRecord(outputSource), responseFormat);
190
+ }
191
+ return { type: "message.completed", role: "assistant", output_text: outputText, format: responseFormat };
192
+ }
193
+ function completionStateFromRecord(data) {
194
+ const value = ownDataProperty(data, "completionState");
195
+ return value === "complete" || value === "generating" || value === "stopped" || value === "partial" || value === "unknown"
196
+ ? value
197
+ : undefined;
198
+ }
199
+ function generationActiveFromRecord(data) {
200
+ const value = ownDataProperty(data, "generationActive");
201
+ return typeof value === "boolean" ? value : undefined;
163
202
  }
164
203
  function runStateFromResult(result, interruptions) {
165
204
  const resumable = interruptions.some(interruption => interruption.resume.supported);
@@ -168,6 +207,17 @@ function runStateFromResult(result, interruptions) {
168
207
  id: firstResume?.supported === true && firstResume.stateId !== undefined ? firstResume.stateId : `run_${Date.now().toString(36)}`,
169
208
  resumable
170
209
  };
210
+ const operationId = readOperationId(result.data);
211
+ const handle = readOperationHandle(result.data);
212
+ if (operationId !== undefined) {
213
+ state.operationId = operationId;
214
+ // Use the durable identity for correlation without changing the runner's
215
+ // resume-policy bit: recovery is collect-only through the operation
216
+ // handle, not permission to replay the high-level workflow.
217
+ state.id = operationId;
218
+ }
219
+ if (handle !== undefined)
220
+ state.handle = handle;
171
221
  const thread = threadRefFromContext(result.context);
172
222
  if (thread !== undefined)
173
223
  state.thread = thread;
@@ -179,13 +229,13 @@ function runStateFromResult(result, interruptions) {
179
229
  state.completionState = completionState;
180
230
  return state;
181
231
  }
182
- function inProgressItem(outputText, completionState, generationActive) {
232
+ function inProgressItem(outputText, completionState, generationActive, responseFormat) {
183
233
  const item = {
184
234
  type: "message.in_progress",
185
235
  role: "assistant",
186
236
  output_text: outputText,
187
237
  preview: outputText.length > 160 ? `${outputText.slice(0, 159)}...` : outputText,
188
- format: "markdown",
238
+ format: responseFormat,
189
239
  textLength: outputText.length,
190
240
  textHash: hashText(outputText)
191
241
  };
@@ -195,46 +245,168 @@ function inProgressItem(outputText, completionState, generationActive) {
195
245
  item.generationActive = generationActive;
196
246
  return item;
197
247
  }
198
- function readCompletionState(data) {
199
- if (!isRecord(data))
248
+ function responseFormatForOutput(data, outputSource) {
249
+ // A top-level responseFormat is an explicit result contract and therefore
250
+ // remains authoritative even when the output text is nested below it.
251
+ const topLevel = ownDataProperty(data, "responseFormat");
252
+ if (isResponseFormat(topLevel))
253
+ return topLevel;
254
+ // Legacy format fields are only meaningful when co-located with the record
255
+ // whose text won the bounded output traversal. Never borrow one from an
256
+ // unrelated metadata/configuration branch.
257
+ const local = responseFormatFromRecord(outputSource);
258
+ return local ?? "markdown";
259
+ }
260
+ function responseFormatFromRecord(data) {
261
+ if (data === undefined)
200
262
  return undefined;
201
- const value = data.completionState;
263
+ const explicit = ownDataProperty(data, "responseFormat");
264
+ if (isResponseFormat(explicit))
265
+ return explicit;
266
+ const local = ownDataProperty(data, "format");
267
+ return isResponseFormat(local) ? local : undefined;
268
+ }
269
+ function isResponseFormat(value) {
270
+ return value === "markdown"
271
+ || value === "text"
272
+ || value === "normalized_text"
273
+ || value === "visible_text"
274
+ || value === "html"
275
+ || value === "blocks"
276
+ || value === "all";
277
+ }
278
+ function readCompletionState(data, seen = new WeakSet(), depth = 0, budget = { remaining: MAX_RESULT_TRAVERSAL_NODES }) {
279
+ if (!enterResultRecord(data, seen, depth, budget))
280
+ return undefined;
281
+ const value = ownDataProperty(data, "completionState");
202
282
  if (value === "complete" || value === "generating" || value === "stopped" || value === "partial" || value === "unknown") {
203
283
  return value;
204
284
  }
205
- for (const nested of Object.values(data)) {
206
- const nestedState = readCompletionState(nested);
285
+ for (const nested of ownDataValues(data)) {
286
+ const nestedState = readCompletionState(nested, seen, depth + 1, budget);
207
287
  if (nestedState !== undefined)
208
288
  return nestedState;
209
289
  }
210
290
  return undefined;
211
291
  }
212
- function readSubmissionState(data) {
213
- if (!isRecord(data))
292
+ function readSubmissionState(data, seen = new WeakSet(), depth = 0, budget = { remaining: MAX_RESULT_TRAVERSAL_NODES }) {
293
+ if (!enterResultRecord(data, seen, depth, budget))
214
294
  return undefined;
215
- const value = data.submissionState;
295
+ const value = ownDataProperty(data, "submissionState");
216
296
  if (value === "not_submitted" || value === "submitted" || value === "submitted_unconfirmed" || value === "submitted_generating") {
217
297
  return value;
218
298
  }
219
- for (const nested of Object.values(data)) {
220
- const nestedState = readSubmissionState(nested);
299
+ for (const nested of ownDataValues(data)) {
300
+ const nestedState = readSubmissionState(nested, seen, depth + 1, budget);
221
301
  if (nestedState !== undefined)
222
302
  return nestedState;
223
303
  }
224
304
  return undefined;
225
305
  }
226
- function readGenerationActive(data) {
227
- if (!isRecord(data))
306
+ function readGenerationActive(data, seen = new WeakSet(), depth = 0, budget = { remaining: MAX_RESULT_TRAVERSAL_NODES }) {
307
+ if (!enterResultRecord(data, seen, depth, budget))
228
308
  return undefined;
229
- if (typeof data.generationActive === "boolean")
230
- return data.generationActive;
231
- for (const nested of Object.values(data)) {
232
- const value = readGenerationActive(nested);
309
+ const generationActive = ownDataProperty(data, "generationActive");
310
+ if (typeof generationActive === "boolean")
311
+ return generationActive;
312
+ for (const nested of ownDataValues(data)) {
313
+ const value = readGenerationActive(nested, seen, depth + 1, budget);
233
314
  if (value !== undefined)
234
315
  return value;
235
316
  }
236
317
  return undefined;
237
318
  }
319
+ function readOperationId(data) {
320
+ const value = ownDataProperty(data, "operationId");
321
+ return typeof value === "string"
322
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value)
323
+ ? value
324
+ : undefined;
325
+ }
326
+ function readRequestDigest(data) {
327
+ const value = ownDataProperty(data, "requestDigest");
328
+ return typeof value === "string" && /^hmac-sha256:[0-9a-f]{64}$/u.test(value)
329
+ ? value
330
+ : undefined;
331
+ }
332
+ function readOperationHandle(data) {
333
+ const value = ownDataProperty(data, "handle");
334
+ if (!isRecord(value))
335
+ return undefined;
336
+ const operationId = ownDataProperty(value, "operationId");
337
+ const requestDigest = ownDataProperty(value, "requestDigest");
338
+ const schemaVersion = ownDataProperty(value, "schemaVersion");
339
+ const surface = ownDataProperty(value, "surface");
340
+ const revision = ownDataProperty(value, "revision");
341
+ const phase = ownDataProperty(value, "phase");
342
+ const mutationBoundary = ownDataProperty(value, "mutationBoundary");
343
+ const targetBindingDigest = ownDataProperty(value, "targetBindingDigest");
344
+ if (schemaVersion !== "chatgpt.browser_control.operation_handle.v1"
345
+ || typeof operationId !== "string"
346
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(operationId)
347
+ || typeof requestDigest !== "string"
348
+ || !/^hmac-sha256:[0-9a-f]{64}$/u.test(requestDigest)
349
+ || (surface !== "chat" && surface !== "work")
350
+ || !Number.isSafeInteger(revision)
351
+ || revision < 1
352
+ || !["prepared", "handoff_pending", "ready", "send_pending", "submitted", "generating", "capturing", "completed", "uncertain"].includes(String(phase))
353
+ || !["none", "handoff_may_have_occurred", "send_may_have_occurred", "control_may_have_occurred"].includes(String(mutationBoundary))
354
+ || (targetBindingDigest !== undefined
355
+ && (typeof targetBindingDigest !== "string" || !/^hmac-sha256:[0-9a-f]{64}$/u.test(targetBindingDigest)))) {
356
+ return undefined;
357
+ }
358
+ return Object.freeze({
359
+ schemaVersion,
360
+ operationId,
361
+ requestDigest,
362
+ surface,
363
+ revision: revision,
364
+ phase: phase,
365
+ mutationBoundary: mutationBoundary,
366
+ ...(targetBindingDigest === undefined ? {} : { targetBindingDigest })
367
+ });
368
+ }
369
+ /** Never invoke a getter while extracting operation metadata from result data. */
370
+ function ownDataProperty(value, key) {
371
+ if (!isRecord(value))
372
+ return undefined;
373
+ try {
374
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
375
+ return descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
376
+ }
377
+ catch {
378
+ return undefined;
379
+ }
380
+ }
381
+ /** Enumerate data values without invoking getters or trusting proxy traps. */
382
+ function ownDataValues(value) {
383
+ if (!isRecord(value))
384
+ return [];
385
+ try {
386
+ const descriptors = Object.getOwnPropertyDescriptors(value);
387
+ const values = [];
388
+ for (const key of Reflect.ownKeys(descriptors)) {
389
+ if (typeof key !== "string")
390
+ continue;
391
+ const descriptor = descriptors[key];
392
+ if (descriptor !== undefined && "value" in descriptor)
393
+ values.push(descriptor.value);
394
+ }
395
+ return values;
396
+ }
397
+ catch {
398
+ return [];
399
+ }
400
+ }
401
+ function enterResultRecord(value, seen, depth, budget) {
402
+ if (!isRecord(value) || depth > MAX_RESULT_TRAVERSAL_DEPTH || budget.remaining <= 0)
403
+ return false;
404
+ if (seen.has(value))
405
+ return false;
406
+ seen.add(value);
407
+ budget.remaining -= 1;
408
+ return true;
409
+ }
238
410
  function hashText(value) {
239
411
  let hash = 2166136261;
240
412
  for (let index = 0; index < value.length; index += 1) {
@@ -2,6 +2,7 @@ import type { AttachedFile, ChatGPTExperience, CommandResult, CommandStatus, Com
2
2
  import type { RunReportOptions } from "../commands/reports.js";
3
3
  import type { UntrustedOutputReturnEnvelope } from "../safety/untrusted-output.js";
4
4
  import type { ChatGPTRunStream } from "./stream.js";
5
+ import type { OperationHandleV1 } from "../operations/types.js";
5
6
  export type ChatGPTThreadSelector = {
6
7
  type: "new";
7
8
  } | {
@@ -106,6 +107,8 @@ export type ChatGPTInputItem = {
106
107
  };
107
108
  export type ChatGPTRunInput = string | {
108
109
  input: string | ChatGPTInputItem[];
110
+ /** Caller-owned durable identity; opts this invocation into operations.run. */
111
+ operationId?: string;
109
112
  thread?: ChatGPTThreadSelector;
110
113
  existingTab?: BootstrapArgs["existingTab"];
111
114
  preferExistingTab?: boolean;
@@ -240,6 +243,8 @@ export type ChatGPTConfirmation = {
240
243
  export type ChatGPTRunState = {
241
244
  id: string;
242
245
  resumable: boolean;
246
+ operationId?: string;
247
+ handle?: OperationHandleV1;
243
248
  thread?: ChatGPTThreadRef;
244
249
  nextStepId?: string;
245
250
  submissionState?: SubmissionState;
@@ -248,6 +253,11 @@ export type ChatGPTRunState = {
248
253
  export type ChatGPTRunData<TOutput = string> = {
249
254
  finalOutput?: TOutput;
250
255
  outputText: string;
256
+ /** Effective caller-owned operation identity when the transactional path was selected. */
257
+ operationId?: string;
258
+ /** Durable locator returned by the transactional operation path. */
259
+ handle?: OperationHandleV1;
260
+ requestDigest?: string;
251
261
  untrustedOutput?: UntrustedOutputReturnEnvelope;
252
262
  thread?: ChatGPTThreadRef;
253
263
  downloads?: DownloadedFileSummary[];
@@ -290,6 +300,8 @@ export type ChatGPTResponse = {
290
300
  submissionState?: SubmissionState;
291
301
  completionState?: CompletionState;
292
302
  generationActive?: boolean;
303
+ operationId?: string;
304
+ handle?: OperationHandleV1;
293
305
  untrustedOutput?: UntrustedOutputReturnEnvelope;
294
306
  unsupported?: UnsupportedField[];
295
307
  };
@@ -0,0 +1,149 @@
1
+ import { ProcessTabCoordinator, type CoordinatorAcquisitionContext, type CoordinatorPriority } from "./tab-coordinator.js";
2
+ import type { OperationRuntimeContext } from "./operation-context.js";
3
+ import { type CoordinatedBrowserOptions } from "./coordinated-browser.js";
4
+ import type { RuntimeEnv } from "../types.js";
5
+ /**
6
+ * Routing keeps the implementation classes visible while also exposing a
7
+ * binary browser-free/coordinator-routed disposition for the acceptance
8
+ * contract. Legacy command implementations still run directly, but their
9
+ * page and browser method calls use the bounded coordinated runtime facade.
10
+ * That does not make the whole command or its polling loop one coordinator
11
+ * transaction.
12
+ *
13
+ * `operation_opt_in` means that an explicit caller-owned operation identity is
14
+ * handled by an operation-aware public facade. It does not authorize wrapping
15
+ * the whole public command in a coordinator callback.
16
+ */
17
+ export type CommandRoutingClass = "browser_free" | "operation_opt_in" | "legacy_page_facade" | "legacy_browser_unrouted" | "coordinator_entrypoint";
18
+ /** The complete routing inventory is intentionally explicit and reviewable. */
19
+ export declare const COMMAND_ROUTING_INVENTORY: Readonly<{
20
+ browserFree: readonly ["backend.version", "backend.health", "backend.capabilities", "backend.hello", "runner.plan", "createReport", "reports.create", "reports.redact", "reports.summarize", "commands", "describe", "help", "redacted-run-report", "files.preflight", "projects.sources.planAdd", "operations.inspect"];
21
+ /**
22
+ * These commands opt into the operation facade only when an operation
23
+ * identity is present. With no identity they retain their legacy path.
24
+ */
25
+ operationOptIn: readonly ["runner.run", "runner.stream", "responses.create", "ask", "askInThread", "askWithFiles", "askAndDownload", "work.start", "work.steer", "operations.submit", "operations.collect", "operations.control"];
26
+ /**
27
+ * Legacy commands retain their public workflow and now receive a
28
+ * coordinator-backed PageLike/BrowserLike facade. This is a method-level
29
+ * guarantee: it does not turn the whole command or its polling loop into a
30
+ * single actor callback.
31
+ */
32
+ legacyPageFacade: readonly ["runMessages", "openThread", "copyLatest", "readLatest", "downloadLatest", "runPlan", "new-ask-read", "find-open-ask-read", "find-open-copy-latest", "attach-ask-read", "ask-and-download", "two-turn", "doctor-upload", "doctor", "session.bootstrap", "experience.detect", "experience.open", "configuration.inspect", "configuration.apply", "work.status", "work.wait", "work.readLatest", "threads.new", "threads.search", "threads.open", "messages.compose", "messages.submit", "messages.ask", "messages.wait", "messages.readLatest", "messages.status", "messages.stop", "messages.waitAndRead", "artifacts.listLatest", "artifacts.wait", "artifacts.downloadLatest", "files.attach", "files.downloadLatest", "projects.sources.list", "projects.sources.add", "response.copy", "modes.set", "modes.get", "tools.select"];
33
+ /**
34
+ * Browser acquisition seams not covered by the facade remain explicit
35
+ * migration gaps. The current command surface has none; discovery is
36
+ * coordinated in browser/attach.ts.
37
+ */
38
+ legacyBrowserUnrouted: readonly [];
39
+ /**
40
+ * No backend or sequence command currently invokes
41
+ * routeCommandBrowserTransaction. Keep this category explicit so a future
42
+ * migration cannot silently turn a classification into an enforcement
43
+ * claim. A command may enter this list only after its own bounded DOM seam
44
+ * is implemented and tested.
45
+ */
46
+ coordinatorEntrypoint: readonly [];
47
+ }>;
48
+ type InventoryCommand = typeof COMMAND_ROUTING_INVENTORY.browserFree[number] | typeof COMMAND_ROUTING_INVENTORY.operationOptIn[number] | typeof COMMAND_ROUTING_INVENTORY.legacyPageFacade[number] | typeof COMMAND_ROUTING_INVENTORY.legacyBrowserUnrouted[number] | typeof COMMAND_ROUTING_INVENTORY.coordinatorEntrypoint[number];
49
+ type LegacyBrowserCommand = typeof COMMAND_ROUTING_INVENTORY.legacyBrowserUnrouted[number];
50
+ /**
51
+ * The operation-aware public dispatch seams currently implemented by the
52
+ * client/backend path. This is separate from the coordinator entrypoint
53
+ * inventory: these commands route into the operation facade, whose adapter
54
+ * owns its own short tab transactions, rather than calling the generic helper
55
+ * below around the whole command.
56
+ */
57
+ export declare const OPERATION_AWARE_DISPATCH_COMMANDS: readonly ["runner.run", "runner.stream", "responses.create", "ask", "askInThread", "askWithFiles", "askAndDownload", "work.start", "work.steer", "operations.submit", "operations.collect", "operations.control"];
58
+ export type CommandRoutingGap = Readonly<{
59
+ command: LegacyBrowserCommand;
60
+ status: "legacy_browser_unrouted";
61
+ owner: string;
62
+ requiredSeam: "bounded_tab_transaction";
63
+ reason: "legacy_command_dispatch_has_no_operation_aware_tab_seam";
64
+ }>;
65
+ export declare const COMMAND_ROUTING_GAPS: readonly CommandRoutingGap[];
66
+ /** Return undefined for a command that has not been explicitly classified. */
67
+ export declare function classifyCommandRouting(command: string): CommandRoutingClass | undefined;
68
+ export declare function isBrowserFreeCommand(command: string): boolean;
69
+ export declare function isOperationOptInCommand(command: string): boolean;
70
+ export declare function isLegacyBrowserUnroutedCommand(command: string): boolean;
71
+ export declare function isLegacyPageFacadeCommand(command: string): boolean;
72
+ export declare function isCoordinatorEntrypointCommand(command: string): boolean;
73
+ /**
74
+ * The public acceptance contract intentionally collapses the implementation
75
+ * classes into the two meaningful routing dispositions. `operation_opt_in`
76
+ * is coordinator-routed by its operation adapter; the legacy page facade is
77
+ * coordinator-routed by the bounded environment adapter below. An explicit
78
+ * `legacy_browser_unrouted` entry is a migration gap and therefore has no
79
+ * safe disposition.
80
+ */
81
+ export type CommandRoutingDisposition = "browser_free" | "coordinator_routed";
82
+ export declare function commandRoutingDisposition(command: string): CommandRoutingDisposition | undefined;
83
+ export declare function isCoordinatorRoutedCommand(command: string): boolean;
84
+ /** Options for the bounded legacy command adapter. */
85
+ export type CommandRoutingAdapterOptions = Readonly<Pick<CoordinatedBrowserOptions, "coordinator" | "owner">>;
86
+ /**
87
+ * Prepare the environment for one command invocation.
88
+ *
89
+ * This is deliberately an adapter rather than a whole-command coordinator
90
+ * transaction. `coordinateRuntimeEnv` returns PageLike/BrowserLike facades
91
+ * whose individual browser calls acquire the process-scoped actor for a
92
+ * bounded operation and release it before the next await. Consequently a
93
+ * command may wait for generation, poll, write a journal, or invoke caller
94
+ * code without retaining a tab actor. The coordinated browser/page values
95
+ * are copied back into the supplied invocation environment so legacy command
96
+ * mutations (notably `session.bootstrap`) remain visible to later sequence
97
+ * steps. Calling this on an already-coordinated environment is idempotent
98
+ * through the wrapper caches.
99
+ */
100
+ export declare function routeCommandRuntimeEnv(command: string, env: RuntimeEnv, options?: CommandRoutingAdapterOptions): RuntimeEnv;
101
+ /**
102
+ * Run one command with the bounded legacy environment adapter.
103
+ *
104
+ * The callback is invoked outside any coordinator callback. The returned
105
+ * PageLike/BrowserLike values enforce short method-level transactions, so a
106
+ * caller callback or a generation/poll sleep cannot deadlock a nested actor.
107
+ */
108
+ export declare function routeCommandExecution<T>(command: string, env: RuntimeEnv, callback: (routedEnv: RuntimeEnv) => T | PromiseLike<T>, options?: CommandRoutingAdapterOptions): Promise<T>;
109
+ export type CommandRoutingContext<Page extends object = object> = Readonly<{
110
+ coordinator: ProcessTabCoordinator;
111
+ runtimeContext: OperationRuntimeContext<Page>;
112
+ priority?: CoordinatorPriority;
113
+ signal?: AbortSignal;
114
+ deadlineAt?: number;
115
+ timeoutMs?: number;
116
+ label?: string;
117
+ }>;
118
+ export type CommandRoutingErrorCode = "unclassified_command" | "coordinator_context_required" | "exact_ownership_unavailable" | "legacy_command_unrouted" | "legacy_page_facade" | "operation_facade_managed" | "operation_routing_unavailable";
119
+ /** Stable error for callers that try to route without enough ownership proof. */
120
+ export declare class CommandRoutingError extends Error {
121
+ readonly code: CommandRoutingErrorCode;
122
+ constructor(code: CommandRoutingErrorCode);
123
+ }
124
+ /**
125
+ * Execute one already-bounded browser transaction for an explicit coordinator
126
+ * entrypoint. The callback is intentionally the transaction boundary, not
127
+ * the command or request boundary. Callers must keep generation waits,
128
+ * polling sleeps, file hashing/transfers, journal writes, and report/caller
129
+ * callbacks outside this helper.
130
+ */
131
+ export declare function routeCommandBrowserTransaction<T, Page extends object = object>(command: string, context: CommandRoutingContext<Page> | undefined, callback: (acquisition: CoordinatorAcquisitionContext | undefined) => T | PromiseLike<T>): Promise<T>;
132
+ /** Return a readonly snapshot useful for diagnostics and inventory tests. */
133
+ export declare function commandRoutingInventory(): Readonly<Record<string, CommandRoutingClass>>;
134
+ /**
135
+ * Return true when a request carries one of the reserved caller-owned
136
+ * operation identity fields. This is intentionally structural and never
137
+ * reads or serializes prompt/instruction values.
138
+ */
139
+ export declare function hasOperationIdentity(value: unknown): boolean;
140
+ /**
141
+ * Backend and sequence dispatchers call this before direct command dispatch.
142
+ * Legacy callers without an operation identity remain byte-for-byte on the
143
+ * existing path. An operation-aware request is allowed only for explicitly
144
+ * migrated facade commands (or browser-free diagnostics); all other browser
145
+ * commands fail before their handler can touch the browser.
146
+ */
147
+ export declare function assertOperationAwareDispatchAllowed(command: string, payload: unknown): void;
148
+ export type ClassifiedCommand = InventoryCommand;
149
+ export {};