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,9 +3,17 @@ import { resultError, resultOk } from "../errors.js";
3
3
  import { contextFromPage } from "./context.js";
4
4
  import { preflightFiles } from "./files.js";
5
5
  import { bootstrap } from "./session.js";
6
+ import { coordinatedEventRegistrationBarrier } from "../runtime/coordinated-page.js";
6
7
  const CHATGPT_ORIGIN = "https://chatgpt.com";
7
8
  const DEFAULT_PROJECT_SOURCE_BATCH_SIZE = 10;
8
9
  const PROJECT_SOURCE_CANDIDATE_LIMIT = 20;
10
+ // The chooser transition is a short UI handoff. It must not inherit the much
11
+ // longer file-handoff/refresh timeout: doing so can leave a rejected browser
12
+ // event pending for minutes and can make us try a second UI path after an
13
+ // ambiguous first action. The public timeout can shorten this budget, but
14
+ // never extend it.
15
+ const PROJECT_SOURCE_CHOOSER_TRANSITION_TIMEOUT_MS = 2500;
16
+ const PROJECT_SOURCE_CHOOSER_POLL_INTERVAL_MS = 25;
9
17
  export function normalizeProjectSourcesUrl(value) {
10
18
  let parsed;
11
19
  try {
@@ -415,16 +423,97 @@ async function uploadProjectSourceBatch(page, batch, timeoutMs) {
415
423
  if (typeof page.waitForEvent !== "function") {
416
424
  throw new Error("The active Project Sources page does not expose file chooser events.");
417
425
  }
418
- let chooserPromise = waitForFileChooser(page, timeoutMs);
419
- await clickProjectSourceControl(page, localeLabels.projectSourcesAddSource, "button", timeoutMs);
420
- const opened = await raceFileChooserOpen(chooserPromise, page, 300);
421
- if (!opened) {
422
- chooserPromise = waitForFileChooser(page, timeoutMs);
423
- await clickProjectSourceControl(page, localeLabels.projectSourcesUploadFiles, "button", timeoutMs);
426
+ const chooserTransitionTimeoutMs = Math.min(timeoutMs, PROJECT_SOURCE_CHOOSER_TRANSITION_TIMEOUT_MS);
427
+ const chooserDeadline = Date.now() + chooserTransitionTimeoutMs;
428
+ // Validate the first visible mutation before subscribing to the chooser.
429
+ // A missing or ambiguous control must not leave a 2.5s bridge waiter alive.
430
+ const addSource = await waitForUniqueVisibleProjectSourceControl(page, localeLabels.projectSourcesAddSource, "button", chooserDeadline);
431
+ if (addSource.kind !== "ready") {
432
+ throw new Error(projectSourceControlDiscoveryMessage(addSource, "button", localeLabels.projectSourcesAddSource));
424
433
  }
425
- const chooser = await chooserPromise;
426
- await chooser.setFiles(paths);
427
- return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
434
+ const remainingChooserTimeoutMs = chooserDeadline - Date.now();
435
+ if (remainingChooserTimeoutMs <= 0) {
436
+ throw new Error("Project Sources Add source control was not ready before the chooser transition deadline.");
437
+ }
438
+ // Start exactly one handled waiter synchronously immediately before the
439
+ // first visible mutation. The listener is therefore installed before the
440
+ // click can deliver a chooser event, while synchronous bridge throws are
441
+ // converted into an already-handled terminal outcome.
442
+ const chooserWait = startFileChooserWait(page, remainingChooserTimeoutMs);
443
+ // Fence coordinated provider registration and drain already-scheduled
444
+ // settlement before crossing the first visible mutation boundary.
445
+ const beforeAddOutcome = await settleChooserBeforeMutation(chooserWait, chooserDeadline);
446
+ if (beforeAddOutcome !== undefined) {
447
+ throw chooserOutcomeError(beforeAddOutcome, undefined, "Add source");
448
+ }
449
+ let addSourceError;
450
+ try {
451
+ await clickProjectSourceControlLocator(page, addSource.locator, chooserDeadline);
452
+ }
453
+ catch (error) {
454
+ addSourceError = error;
455
+ }
456
+ // A click can deliver the browser gesture and still reject at the bridge
457
+ // boundary. Reconcile a late chooser success instead of issuing a second
458
+ // Add/Upload action. A rejection is returned deterministically as the
459
+ // chooser outcome, with the click failure retained as context.
460
+ if (addSourceError !== undefined) {
461
+ const outcome = await awaitFileChooserOutcome(chooserWait, chooserDeadline);
462
+ if (outcome.kind === "success") {
463
+ await setProjectSourceChooserFiles(outcome.chooser, paths, timeoutMs);
464
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
465
+ }
466
+ throw chooserOutcomeError(outcome, addSourceError, "Add source");
467
+ }
468
+ // Let a chooser event delivered during the Add click finish its handled
469
+ // microtask before deciding whether the optional menu path is necessary.
470
+ await Promise.resolve();
471
+ const afterAddOutcome = chooserWait.outcome;
472
+ if (afterAddOutcome?.kind === "success") {
473
+ await setProjectSourceChooserFiles(afterAddOutcome.chooser, paths, timeoutMs);
474
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
475
+ }
476
+ if (afterAddOutcome?.kind === "rejected") {
477
+ throw chooserOutcomeError(afterAddOutcome, undefined, "Add source");
478
+ }
479
+ const uploadControl = await waitForChooserOrUploadControl(page, localeLabels.projectSourcesUploadFiles, "button", chooserWait, chooserDeadline);
480
+ if (uploadControl.kind === "chooser") {
481
+ if (uploadControl.outcome.kind === "success") {
482
+ await setProjectSourceChooserFiles(uploadControl.outcome.chooser, paths, timeoutMs);
483
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
484
+ }
485
+ throw chooserOutcomeError(uploadControl.outcome, undefined, "Add source");
486
+ }
487
+ if (uploadControl.kind !== "ready") {
488
+ throw new Error(uploadControl.kind === "timeout"
489
+ ? uploadControl.message
490
+ : projectSourceControlDiscoveryMessage(uploadControl, "button", localeLabels.projectSourcesUploadFiles));
491
+ }
492
+ // Final settled-outcome gate: discovery may have yielded while the
493
+ // chooser promise settled. Do not click Upload when the original waiter
494
+ // has already produced either terminal outcome.
495
+ await Promise.resolve();
496
+ const settledBeforeUpload = chooserWait.outcome;
497
+ if (settledBeforeUpload?.kind === "success") {
498
+ await setProjectSourceChooserFiles(settledBeforeUpload.chooser, paths, timeoutMs);
499
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
500
+ }
501
+ if (settledBeforeUpload?.kind === "rejected") {
502
+ throw chooserOutcomeError(settledBeforeUpload, undefined, "Add source");
503
+ }
504
+ let uploadError;
505
+ try {
506
+ await clickProjectSourceControlLocator(page, uploadControl.locator, chooserDeadline);
507
+ }
508
+ catch (error) {
509
+ uploadError = error;
510
+ }
511
+ const outcome = await awaitFileChooserOutcome(chooserWait, chooserDeadline);
512
+ if (outcome.kind === "success") {
513
+ await setProjectSourceChooserFiles(outcome.chooser, paths, timeoutMs);
514
+ return resultOk({ files: batch.files.map(file => ({ name: file.name, bytes: file.bytes })) }, await contextFromPage(page));
515
+ }
516
+ throw chooserOutcomeError(outcome, uploadError, "Upload files");
428
517
  }
429
518
  catch (error) {
430
519
  return {
@@ -457,36 +546,241 @@ async function clickSourcesTabIfAvailable(page, timeoutMs) {
457
546
  }
458
547
  }
459
548
  async function clickProjectSourceControl(page, labels, role, timeoutMs) {
460
- const locator = page.getByRole?.(role, { name: anyLabelPattern(labels) });
461
- if (locator !== undefined && await locatorCount(locator) > 0) {
462
- await (locator.first?.() ?? locator).click?.({ timeout: Math.min(timeoutMs, 10000) });
549
+ const deadline = Date.now() + Math.max(0, timeoutMs);
550
+ // The Sources tab is a navigation aid, not part of the chooser transition.
551
+ // Keep its best-effort lookup bounded to one DOM observation so a page that
552
+ // lacks the tab still reaches the existing selector-drift fallback promptly.
553
+ const candidate = await discoverUniqueVisibleProjectSourceControl(page, labels, role);
554
+ if (candidate.kind !== "ready") {
555
+ throw new Error(projectSourceControlDiscoveryMessage(candidate, role, labels));
556
+ }
557
+ await clickProjectSourceControlLocator(page, candidate.locator, deadline);
558
+ if (role === "tab") {
463
559
  await page.waitForTimeout?.(250);
464
- return;
465
560
  }
466
- const selector = labels
467
- .map(label => `button[aria-label*='${cssString(label)}'], [role='${role}'][aria-label*='${cssString(label)}']`)
468
- .join(", ");
469
- const fallback = page.locator?.(selector);
470
- if (fallback !== undefined && await locatorCount(fallback) > 0) {
471
- await (fallback.first?.() ?? fallback).click?.({ timeout: Math.min(timeoutMs, 10000) });
472
- await page.waitForTimeout?.(250);
473
- return;
561
+ }
562
+ function startFileChooserWait(page, timeoutMs) {
563
+ const wait = {
564
+ promise: Promise.resolve({
565
+ kind: "rejected",
566
+ error: new Error("Project Sources file chooser wait was not initialized.")
567
+ })
568
+ };
569
+ let rawWait;
570
+ try {
571
+ // Invoke waitForEvent in this call stack. Deferring registration through a
572
+ // Promise.resolve().then() can let an immediately-triggered Add click race
573
+ // the listener and lose the chooser event.
574
+ rawWait = page.waitForEvent?.("filechooser", { timeout: timeoutMs, timeoutMs });
575
+ }
576
+ catch (error) {
577
+ const outcome = { kind: "rejected", error };
578
+ wait.outcome = outcome;
579
+ wait.promise = Promise.resolve(outcome);
580
+ return wait;
581
+ }
582
+ const registration = coordinatedEventRegistrationBarrier(rawWait);
583
+ if (registration !== undefined)
584
+ wait.registration = registration;
585
+ wait.promise = Promise.resolve(rawWait).then(rawChooser => {
586
+ const outcome = isProjectSourceFileChooserLike(rawChooser)
587
+ ? { kind: "success", chooser: rawChooser }
588
+ : {
589
+ kind: "rejected",
590
+ error: new Error("Project Sources file chooser did not expose setFiles().")
591
+ };
592
+ wait.outcome = outcome;
593
+ return outcome;
594
+ }, error => {
595
+ const outcome = { kind: "rejected", error };
596
+ wait.outcome = outcome;
597
+ return outcome;
598
+ });
599
+ return wait;
600
+ }
601
+ /**
602
+ * Prove that a coordinated event listener finished registration before a
603
+ * click. One host turn then drains any outcome that was already scheduled;
604
+ * such an outcome cannot causally belong to the not-yet-issued click and
605
+ * therefore blocks the handoff.
606
+ */
607
+ async function settleChooserBeforeMutation(chooserWait, deadline) {
608
+ if (chooserWait.outcome !== undefined)
609
+ return chooserWait.outcome;
610
+ if (chooserWait.registration !== undefined) {
611
+ const remainingMs = deadline - Date.now();
612
+ if (remainingMs <= 0)
613
+ return { kind: "timeout" };
614
+ let timer;
615
+ const registered = await Promise.race([
616
+ chooserWait.registration.then(() => true),
617
+ new Promise(resolve => { timer = setTimeout(() => resolve(false), remainingMs); })
618
+ ]).finally(() => {
619
+ if (timer !== undefined)
620
+ clearTimeout(timer);
621
+ });
622
+ if (!registered)
623
+ return { kind: "timeout" };
624
+ }
625
+ if (chooserWait.outcome !== undefined)
626
+ return chooserWait.outcome;
627
+ if (deadline <= Date.now())
628
+ return { kind: "timeout" };
629
+ await new Promise(resolve => setTimeout(resolve, 0));
630
+ if (chooserWait.outcome !== undefined)
631
+ return chooserWait.outcome;
632
+ return deadline <= Date.now() ? { kind: "timeout" } : undefined;
633
+ }
634
+ async function awaitFileChooserOutcome(chooserWait, deadline) {
635
+ if (chooserWait.outcome !== undefined) {
636
+ return chooserWait.outcome;
637
+ }
638
+ const remainingMs = Math.max(0, deadline - Date.now());
639
+ if (remainingMs === 0) {
640
+ return { kind: "timeout" };
641
+ }
642
+ return new Promise(resolve => {
643
+ let finished = false;
644
+ const finish = (outcome) => {
645
+ if (finished)
646
+ return;
647
+ finished = true;
648
+ clearTimeout(timer);
649
+ resolve(outcome);
650
+ };
651
+ const timer = setTimeout(() => finish({ kind: "timeout" }), remainingMs);
652
+ // The chooser promise is always handled and never rejects. The timer is
653
+ // cleared when the event wins, avoiding a dangling transition timer.
654
+ void chooserWait.promise.then(outcome => finish(outcome));
655
+ });
656
+ }
657
+ function projectSourceControlDiscoveryMessage(discovery, role, labels) {
658
+ if (discovery.kind === "invalid") {
659
+ return discovery.message;
660
+ }
661
+ return `Project Sources ${role} was not uniquely visible for labels: ${labels.join(", ")}`;
662
+ }
663
+ async function waitForUniqueVisibleProjectSourceControl(page, labels, role, deadline) {
664
+ while (true) {
665
+ const candidate = await discoverUniqueVisibleProjectSourceControl(page, labels, role);
666
+ if (candidate.kind !== "absent") {
667
+ return candidate;
668
+ }
669
+ const remainingMs = deadline - Date.now();
670
+ if (remainingMs <= 0) {
671
+ return {
672
+ kind: "invalid",
673
+ message: `Project Sources ${role} was not uniquely visible before the chooser transition deadline for labels: ${labels.join(", ")}`
674
+ };
675
+ }
676
+ await waitForProjectSourceTransitionTick(page, Math.min(PROJECT_SOURCE_CHOOSER_POLL_INTERVAL_MS, remainingMs));
677
+ }
678
+ }
679
+ async function discoverUniqueVisibleProjectSourceControl(page, labels, role) {
680
+ const pattern = anyLabelPattern(labels);
681
+ const primary = page.getByRole?.(role, { name: pattern });
682
+ const candidate = primary !== undefined && await locatorCount(primary) > 0
683
+ ? primary
684
+ : page.locator?.(labels
685
+ .map(label => `button[aria-label*='${cssString(label)}'], [role='${role}'][aria-label*='${cssString(label)}']`)
686
+ .join(", "));
687
+ if (candidate === undefined) {
688
+ return { kind: "absent" };
689
+ }
690
+ const count = await locatorCount(candidate);
691
+ if (count === 0) {
692
+ return { kind: "absent" };
693
+ }
694
+ if (count !== 1) {
695
+ return {
696
+ kind: "invalid",
697
+ message: `Project Sources ${role} was not uniquely available for labels: ${labels.join(", ")} (found ${count} controls).`
698
+ };
699
+ }
700
+ if (typeof candidate.isVisible !== "function") {
701
+ return {
702
+ kind: "invalid",
703
+ message: `Project Sources ${role} visibility could not be verified for labels: ${labels.join(", ")}.`
704
+ };
705
+ }
706
+ if (!await candidate.isVisible().catch(() => false)) {
707
+ return {
708
+ kind: "invalid",
709
+ message: `Project Sources ${role} was present but hidden for labels: ${labels.join(", ")}.`
710
+ };
474
711
  }
475
- throw new Error(`Project Sources ${role} was not available for labels: ${labels.join(", ")}`);
712
+ return { kind: "ready", locator: candidate };
476
713
  }
477
- async function waitForFileChooser(page, timeoutMs) {
478
- const rawChooser = await page.waitForEvent?.("filechooser", { timeout: timeoutMs, timeoutMs });
479
- if (rawChooser === null || typeof rawChooser !== "object" || typeof rawChooser.setFiles !== "function") {
480
- throw new Error("Project Sources file chooser did not expose setFiles().");
714
+ async function waitForChooserOrUploadControl(page, labels, role, chooserWait, deadline) {
715
+ while (true) {
716
+ if (chooserWait.outcome !== undefined) {
717
+ return { kind: "chooser", outcome: chooserWait.outcome };
718
+ }
719
+ const candidate = await discoverUniqueVisibleProjectSourceControl(page, labels, role);
720
+ if (candidate.kind !== "absent") {
721
+ // A chooser rejection may race the final control observation. Prefer the
722
+ // already-settled browser outcome over reporting stale menu ambiguity.
723
+ if (chooserWait.outcome !== undefined) {
724
+ return { kind: "chooser", outcome: chooserWait.outcome };
725
+ }
726
+ return candidate;
727
+ }
728
+ const remainingMs = deadline - Date.now();
729
+ if (remainingMs <= 0) {
730
+ return {
731
+ kind: "timeout",
732
+ message: `Project Sources file chooser did not settle and Upload control did not become uniquely visible before the ${PROJECT_SOURCE_CHOOSER_TRANSITION_TIMEOUT_MS}ms chooser transition deadline.`
733
+ };
734
+ }
735
+ await waitForChooserOrTransitionTick(page, chooserWait, Math.min(PROJECT_SOURCE_CHOOSER_POLL_INTERVAL_MS, remainingMs));
481
736
  }
482
- return rawChooser;
483
737
  }
484
- async function raceFileChooserOpen(chooserPromise, page, waitMs) {
485
- return Promise.race([
486
- chooserPromise.then(() => true, () => false),
487
- (page.waitForTimeout?.(waitMs) ?? new Promise(resolve => setTimeout(resolve, waitMs))).then(() => false)
738
+ async function waitForChooserOrTransitionTick(page, chooserWait, waitMs) {
739
+ if (chooserWait.outcome !== undefined)
740
+ return;
741
+ await Promise.race([
742
+ chooserWait.promise.then(() => undefined),
743
+ waitForProjectSourceTransitionTick(page, waitMs)
488
744
  ]);
489
745
  }
746
+ async function waitForProjectSourceTransitionTick(page, waitMs) {
747
+ if (typeof page.waitForTimeout === "function") {
748
+ await page.waitForTimeout(waitMs);
749
+ return;
750
+ }
751
+ await new Promise(resolve => setTimeout(resolve, waitMs));
752
+ }
753
+ async function clickProjectSourceControlLocator(page, locator, deadline) {
754
+ if (typeof locator.click !== "function") {
755
+ throw new Error("Project Sources control does not expose click().");
756
+ }
757
+ const remainingMs = Math.max(1, deadline - Date.now());
758
+ await locator.click({ timeout: Math.min(remainingMs, 10_000) });
759
+ }
760
+ async function setProjectSourceChooserFiles(chooser, paths, timeoutMs) {
761
+ await chooser.setFiles(paths, { timeoutMs });
762
+ }
763
+ function chooserOutcomeError(outcome, clickError, controlLabel) {
764
+ if (outcome.kind === "rejected") {
765
+ const chooserMessage = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
766
+ const clickMessage = clickError === undefined
767
+ ? ""
768
+ : ` ${controlLabel} click also failed: ${clickError instanceof Error ? clickError.message : String(clickError)}`;
769
+ return new Error(`Project Sources file chooser was rejected: ${chooserMessage}.${clickMessage}`);
770
+ }
771
+ if (outcome.kind === "timeout") {
772
+ const clickMessage = clickError === undefined
773
+ ? ""
774
+ : ` ${controlLabel} click failed: ${clickError instanceof Error ? clickError.message : String(clickError)}.`;
775
+ return new Error(`Project Sources file chooser did not settle before the ${PROJECT_SOURCE_CHOOSER_TRANSITION_TIMEOUT_MS}ms chooser transition deadline.${clickMessage}`);
776
+ }
777
+ return new Error(`Project Sources file chooser settled before the ${controlLabel} click; no file handoff was attempted.`);
778
+ }
779
+ function isProjectSourceFileChooserLike(value) {
780
+ return value !== null
781
+ && typeof value === "object"
782
+ && typeof value.setFiles === "function";
783
+ }
490
784
  async function locatorCount(locator) {
491
785
  if (typeof locator.count !== "function") {
492
786
  return 0;
@@ -1,3 +1,5 @@
1
+ import { resultError } from "../errors.js";
2
+ import { assertOperationAwareDispatchAllowed, CommandRoutingError, classifyCommandRouting, routeCommandRuntimeEnv } from "../runtime/command-routing.js";
1
3
  import { downloadLatestArtifact, listLatestArtifacts, waitForArtifact } from "./artifacts.js";
2
4
  import { applyConfiguration, inspectConfiguration } from "./configuration.js";
3
5
  import { detectExperience, openExperience } from "./experience.js";
@@ -43,71 +45,96 @@ export async function runSequenceWithExecutor(plan, executor, env = {}) {
43
45
  return withCommandOutputText({ ...finalResult, steps: stepResults });
44
46
  }
45
47
  export async function executeStep(step, env, previousResults) {
48
+ // The sequence executor is a direct dispatch seam as well as a helper for
49
+ // backend workflows. Keep it fail-closed if a runtime caller supplies a
50
+ // command outside the explicit inventory. This guard does not acquire a
51
+ // coordinator: legacy commands retain their existing behavior, while an
52
+ // operation-aware request may use only an explicitly migrated facade seam.
53
+ if (classifyCommandRouting(step.command) === undefined) {
54
+ return resultError(new CommandRoutingError("unclassified_command"));
55
+ }
56
+ try {
57
+ assertOperationAwareDispatchAllowed(step.command, step.args);
58
+ }
59
+ catch (error) {
60
+ return resultError(error instanceof Error ? error : new Error(String(error)));
61
+ }
62
+ let routedEnv;
63
+ try {
64
+ // Legacy sequence steps receive a fresh coordinated facade. The facade
65
+ // routes each browser/page method through the process-scoped coordinator;
66
+ // this function never wraps the complete step, so waits, polling, report
67
+ // work, and caller callbacks do not retain a tab actor.
68
+ routedEnv = routeCommandRuntimeEnv(step.command, env);
69
+ }
70
+ catch (error) {
71
+ return resultError(error instanceof Error ? error : new Error(String(error)));
72
+ }
46
73
  switch (step.command) {
47
74
  case "session.bootstrap":
48
- return bootstrap(env, step.args);
75
+ return bootstrap(routedEnv, step.args);
49
76
  case "experience.detect":
50
- return detectExperience(env, step.args);
77
+ return detectExperience(routedEnv, step.args);
51
78
  case "experience.open":
52
- return openExperience(env, step.args);
79
+ return openExperience(routedEnv, step.args);
53
80
  case "configuration.inspect":
54
- return inspectConfiguration(env, step.args);
81
+ return inspectConfiguration(routedEnv, step.args);
55
82
  case "configuration.apply":
56
- return applyConfiguration(env, step.args);
83
+ return applyConfiguration(routedEnv, step.args);
57
84
  case "work.start":
58
- return startWork(env, step.args);
85
+ return startWork(routedEnv, step.args);
59
86
  case "work.status":
60
- return workStatus(env, step.args);
87
+ return workStatus(routedEnv, step.args);
61
88
  case "work.wait":
62
- return waitForWork(env, step.args);
89
+ return waitForWork(routedEnv, step.args);
63
90
  case "work.steer":
64
- return steerWork(env, step.args);
91
+ return steerWork(routedEnv, step.args);
65
92
  case "work.readLatest":
66
- return readLatestWork(env, step.args);
93
+ return readLatestWork(routedEnv, step.args);
67
94
  case "threads.search":
68
- return searchThreads(env, step.args);
95
+ return searchThreads(routedEnv, step.args);
69
96
  case "threads.open":
70
- return openThread(env, step.args, previousResults);
97
+ return openThread(routedEnv, step.args, previousResults);
71
98
  case "threads.new":
72
- return newThread(env, step.args);
99
+ return newThread(routedEnv, step.args);
73
100
  case "messages.compose":
74
- return composeMessage(env, step.args);
101
+ return composeMessage(routedEnv, step.args);
75
102
  case "messages.submit":
76
- return submitMessage(env, step.args);
103
+ return submitMessage(routedEnv, step.args);
77
104
  case "messages.ask":
78
- return askMessage(env, step.args);
105
+ return askMessage(routedEnv, step.args);
79
106
  case "messages.wait":
80
- return waitForMessage(env, step.args);
107
+ return waitForMessage(routedEnv, step.args);
81
108
  case "messages.readLatest":
82
- return readLatest(env, step.args);
109
+ return readLatest(routedEnv, step.args);
83
110
  case "messages.status":
84
- return messageStatus(env, step.args);
111
+ return messageStatus(routedEnv, step.args);
85
112
  case "messages.stop":
86
- return stopGeneration(env, step.args);
113
+ return stopGeneration(routedEnv, step.args);
87
114
  case "messages.waitAndRead":
88
- return waitAndRead(env, step.args);
115
+ return waitAndRead(routedEnv, step.args);
89
116
  case "artifacts.listLatest":
90
- return listLatestArtifacts(env, step.args);
117
+ return listLatestArtifacts(routedEnv, step.args);
91
118
  case "artifacts.wait":
92
- return waitForArtifact(env, step.args);
119
+ return waitForArtifact(routedEnv, step.args);
93
120
  case "artifacts.downloadLatest":
94
- return downloadLatestArtifact(env, step.args);
121
+ return downloadLatestArtifact(routedEnv, step.args);
95
122
  case "files.attach":
96
- return attachFiles(env, step.args);
123
+ return attachFiles(routedEnv, step.args);
97
124
  case "files.downloadLatest":
98
- return downloadLatestFile(env, step.args);
125
+ return downloadLatestFile(routedEnv, step.args);
99
126
  case "projects.sources.list":
100
- return listProjectSources(env, step.args);
127
+ return listProjectSources(routedEnv, step.args);
101
128
  case "projects.sources.planAdd":
102
- return buildProjectSourceAddPlan(env, step.args);
129
+ return buildProjectSourceAddPlan(routedEnv, step.args);
103
130
  case "projects.sources.add":
104
- return addProjectSources(env, step.args);
131
+ return addProjectSources(routedEnv, step.args);
105
132
  case "response.copy":
106
- return copyResponse(env, step.args);
133
+ return copyResponse(routedEnv, step.args);
107
134
  case "modes.set":
108
- return setMode(env, step.args);
135
+ return setMode(routedEnv, step.args);
109
136
  case "tools.select":
110
- return selectTool(env, step.args);
137
+ return selectTool(routedEnv, step.args);
111
138
  }
112
139
  }
113
140
  export function normalizePolicy(policy) {
@@ -1,6 +1,7 @@
1
1
  import { attachChatGPTBrowser, isChatGPTUrl, tabIdFromPage } from "../browser/attach.js";
2
2
  import { readPageState } from "../browser/page-state.js";
3
3
  import { resultError, resultOk } from "../errors.js";
4
+ import { unwrapCoordinatedPage } from "../runtime/coordinated-page.js";
4
5
  import { contextFromPage } from "./context.js";
5
6
  export async function bootstrap(env, args = {}) {
6
7
  try {
@@ -71,7 +72,11 @@ export async function verifyTabAffinity(env) {
71
72
  if (env.expectedTabId === undefined || env.page === undefined) {
72
73
  return undefined;
73
74
  }
74
- const actualTabId = tabIdFromPage(env.page);
75
+ // The coordinated facade intentionally snapshots its public identity when
76
+ // it is created so its coordinator key cannot drift underneath queued work.
77
+ // Affinity verification has a different job: it must inspect the provider
78
+ // page that the facade protects and detect a changed/reused tab claim.
79
+ const actualTabId = tabIdFromPage(unwrapCoordinatedPage(env.page));
75
80
  if (actualTabId === env.expectedTabId) {
76
81
  return undefined;
77
82
  }
@@ -99,7 +104,7 @@ export async function verifyTabAffinity(env) {
99
104
  context: await contextFromPage(env.page, tabContext(env, actualTabId))
100
105
  };
101
106
  }
102
- function tabContext(env, actualTabId = tabIdFromPage(env.page)) {
107
+ function tabContext(env, actualTabId = tabIdFromPage(unwrapCoordinatedPage(env.page))) {
103
108
  const tabId = actualTabId ?? env.expectedTabId;
104
109
  return tabId === undefined ? {} : { tabId };
105
110
  }
@@ -10,6 +10,9 @@ import { askMessage, composeMessage, messageStatus, readLatest, submitMessage, w
10
10
  import { ensurePage } from "./session.js";
11
11
  const NEW_WORK_LABELS = localeLabels.newWork;
12
12
  export async function startWork(env, args) {
13
+ if (args.operationId !== undefined) {
14
+ return transactionalWorkRoutingBlocker(args.operationId, "start");
15
+ }
13
16
  const prompt = args.prompt.trim();
14
17
  if (prompt.length === 0) {
15
18
  return {
@@ -172,6 +175,9 @@ export async function waitForWork(env, args = {}) {
172
175
  return markWorkResult(await waitForMessage(env, args), ready.data?.selectorProfile);
173
176
  }
174
177
  export async function steerWork(env, args) {
178
+ if (args.operationId !== undefined || args.handle !== undefined || args.controlActionId !== undefined || args.expectedAssistantTurnId !== undefined) {
179
+ return transactionalWorkRoutingBlocker(args.operationId ?? args.handle?.operationId, "steer");
180
+ }
175
181
  const ready = await requireWork(env);
176
182
  if (!ready.ok)
177
183
  return forwardCommandFailure(ready);
@@ -182,6 +188,31 @@ export async function steerWork(env, args) {
182
188
  ...(args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs })
183
189
  }), ready.data?.selectorProfile);
184
190
  }
191
+ function transactionalWorkRoutingBlocker(operationId, action) {
192
+ const message = `Transactional Work ${action} must be invoked through chatgpt.work.${action}; the low-level command has no operations facade.`;
193
+ const data = action === "start"
194
+ ? {
195
+ task: {},
196
+ submitted: { submitted: false, submissionState: "not_submitted" },
197
+ ...(operationId === undefined ? {} : { operationId })
198
+ }
199
+ : operationId === undefined ? {} : { operationId };
200
+ return {
201
+ ok: false,
202
+ status: "unsupported",
203
+ data,
204
+ warnings: [],
205
+ error: { name: "OperationRoutingError", message, recoverable: false },
206
+ blocker: {
207
+ kind: "unknown",
208
+ code: "transactional_work_requires_client",
209
+ fieldPath: action === "start" ? "operationId" : "handle",
210
+ message,
211
+ resumable: false
212
+ },
213
+ context: { timestamp: new Date().toISOString(), experience: "work" }
214
+ };
215
+ }
185
216
  export async function readLatestWork(env, args = {}) {
186
217
  const ready = await requireWork(env);
187
218
  if (!ready.ok)
@@ -2,6 +2,15 @@ import type { BlockerKind, CommandContext, CommandResult } from "./types.js";
2
2
  type BlockerDetails = Partial<Omit<NonNullable<CommandResult["blocker"]>, "kind" | "message" | "visibleText">>;
3
3
  export declare const BROWSER_BRIDGE_UNAVAILABLE_MESSAGE = "Codex cannot access the ChatGPT browser bridge from this backend process. In an ordinary shell this is expected; for a live Codex Chrome run, assign the Chrome plugin runtime returned by setupBrowserRuntime() to globalThis.agent before using it.";
4
4
  export declare const BROWSER_BRIDGE_REMEDIATION: NonNullable<NonNullable<CommandResult["blocker"]>["remediation"]>;
5
+ /**
6
+ * Read a Node-style errno code without relying on realm-sensitive
7
+ * `instanceof Error` checks or invoking an untrusted accessor.
8
+ *
9
+ * Browser-hosted modules can receive filesystem errors created by the host
10
+ * realm. Those values are genuine Node errno objects but are not necessarily
11
+ * instances of this bundle's `Error` constructor.
12
+ */
13
+ export declare function nodeErrorCode(error: unknown): string | undefined;
5
14
  export declare class ChatGPTControlError extends Error {
6
15
  readonly kind: BlockerKind;
7
16
  readonly recoverable: boolean;
@@ -21,6 +21,28 @@ export const BROWSER_BRIDGE_REMEDIATION = [
21
21
  userActionRequired: true
22
22
  }
23
23
  ];
24
+ /**
25
+ * Read a Node-style errno code without relying on realm-sensitive
26
+ * `instanceof Error` checks or invoking an untrusted accessor.
27
+ *
28
+ * Browser-hosted modules can receive filesystem errors created by the host
29
+ * realm. Those values are genuine Node errno objects but are not necessarily
30
+ * instances of this bundle's `Error` constructor.
31
+ */
32
+ export function nodeErrorCode(error) {
33
+ if (error === null || (typeof error !== "object" && typeof error !== "function"))
34
+ return undefined;
35
+ try {
36
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
37
+ if (descriptor === undefined || !("value" in descriptor) || typeof descriptor.value !== "string") {
38
+ return undefined;
39
+ }
40
+ return descriptor.value;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
24
46
  export class ChatGPTControlError extends Error {
25
47
  kind;
26
48
  recoverable;