@steipete/oracle 0.18.0 → 0.19.0

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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +119 -16
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. package/dist/src/browser/windowsCookies.js +0 -219
@@ -4,23 +4,26 @@ import os from "node:os";
4
4
  import net from "node:net";
5
5
  import { resolveBrowserConfig } from "./config.js";
6
6
  import { copyChromeProfile } from "./profileCopy.js";
7
- import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, connectToRemoteChrome, connectWithNewTab, closeTab, createChromePageTarget, ensureChromePageTargetAfterClose, closeBlankChromeTabs, } from "./chromeLifecycle.js";
7
+ import { BrowserCancellation, withoutBrowserCancellation } from "./cancellation.js";
8
+ import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, positionChromeWindowOnscreen, connectToRemoteChrome, connectWithNewTab, closeTab, createChromePageTarget, ensureChromePageTargetAfterClose, closeBlankChromeTabs, } from "./chromeLifecycle.js";
8
9
  import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
9
- import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
10
+ import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, captureComposerNavigationUrl, assertComposerPlusStayedInPlace, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
10
11
  import { INPUT_SELECTORS } from "./constants.js";
11
12
  import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
12
13
  import { ensureThinkingTime } from "./actions/thinkingTime.js";
14
+ import { throwIfAssistantUiError } from "./actions/assistantResponse.js";
13
15
  import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
14
16
  import { activateDeepResearch, captureDeepResearchTargetKeys, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
15
17
  import { estimateTokenCount, withRetries, delay } from "./utils.js";
16
18
  import { formatElapsed } from "../oracle/format.js";
17
19
  import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY } from "./constants.js";
18
- import { BrowserAutomationError } from "../oracle/errors.js";
20
+ import { BrowserAutomationError, BrowserRunCancelledError } from "../oracle/errors.js";
21
+ import { buildAttachmentBasenameCollisionDetails, findAttachmentBasenameCollisions, formatAttachmentBasenameCollisionMessage, } from "./attachmentValidation.js";
19
22
  import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
20
23
  import { buildConversationTurnCountExpression } from "./conversationTurns.js";
21
24
  import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
22
25
  import { connectionLostUserMessage, isRecoverableChromeDisconnect, probeChromeTargetLiveness, } from "./cdpLiveness.js";
23
- import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
26
+ import { acquireBrowserTabLease } from "./tabLeaseRegistry.js";
24
27
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
25
28
  import { collectGeneratedImageArtifacts } from "./chatgptImages.js";
26
29
  import { collectChatGptFileArtifacts } from "./chatgptFiles.js";
@@ -58,7 +61,7 @@ function isReattachableCaptureError(error) {
58
61
  if (!(error instanceof BrowserAutomationError))
59
62
  return false;
60
63
  const stage = error.details?.stage;
61
- return stage === "assistant-timeout" || stage === "assistant-recheck";
64
+ return (stage === "assistant-timeout" || stage === "assistant-recheck" || stage === "assistant-ui-error");
62
65
  }
63
66
  function classifyPreservedBrowserError(error, headless) {
64
67
  if (headless)
@@ -86,6 +89,11 @@ export function shouldPreserveBrowserOnErrorForTest(error, headless) {
86
89
  export function classifyPreservedBrowserErrorForTest(error, headless) {
87
90
  return classifyPreservedBrowserError(error, headless);
88
91
  }
92
+ function shouldApplyThinkingTimeSelection(config) {
93
+ // Deep Research uses the same effort picker, so research mode must not
94
+ // suppress an explicitly configured thinking-time selection.
95
+ return config.thinkingTime !== undefined;
96
+ }
89
97
  const MAX_CHATGPT_UI_WARNING_CHARS = 300;
90
98
  const MAX_CHATGPT_UI_WARNINGS = 3;
91
99
  function classifyChatGptUiWarningText(text) {
@@ -318,6 +326,17 @@ function hasBrowserErrorCode(error, code) {
318
326
  return (error instanceof BrowserAutomationError &&
319
327
  error.details?.code === code);
320
328
  }
329
+ function assertUniqueAttachmentBasenames(attachments, options) {
330
+ const collisions = findAttachmentBasenameCollisions(attachments);
331
+ if (collisions.length === 0)
332
+ return;
333
+ const collisionDetails = buildAttachmentBasenameCollisionDetails(collisions, (attachment) => attachment.displayPath || attachment.path);
334
+ throw new BrowserAutomationError(formatAttachmentBasenameCollisionMessage(options.subject, collisionDetails.collisions), {
335
+ stage: options.stage,
336
+ code: "attachment-basename-collision",
337
+ ...collisionDetails,
338
+ });
339
+ }
321
340
  async function saveOptionalArtifact(operation, logger) {
322
341
  try {
323
342
  return await operation();
@@ -357,10 +376,12 @@ async function pollGeneratedImageOrTextAssistantResponse(Runtime, timeoutMs, min
357
376
  const deadline = Date.now() + timeoutMs;
358
377
  while (Date.now() < deadline) {
359
378
  let snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId).catch(() => null);
379
+ throwIfAssistantUiError(snapshot);
360
380
  if (!snapshot && typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex)) {
361
381
  const relaxedSnapshot = await readAssistantSnapshot(Runtime, undefined, expectedConversationId).catch(() => null);
362
382
  const relaxedHtml = typeof relaxedSnapshot?.html === "string" ? relaxedSnapshot.html : "";
363
- if (relaxedHtml.includes("/backend-api/estuary/content?id=file_")) {
383
+ if (!relaxedSnapshot?.uiError &&
384
+ relaxedHtml.includes("/backend-api/estuary/content?id=file_")) {
364
385
  snapshot = relaxedSnapshot;
365
386
  }
366
387
  }
@@ -491,6 +512,13 @@ async function runSubmissionWithRecovery({ prompt, attachments, fallbackSubmissi
491
512
  if (fallbackSubmission && isPromptTooLarge && !usedFallbackSubmission) {
492
513
  usedFallbackSubmission = true;
493
514
  logger("[browser] Inline prompt too large; retrying with file uploads.");
515
+ if (fallbackSubmission.prepare) {
516
+ await fallbackSubmission.prepare();
517
+ }
518
+ assertUniqueAttachmentBasenames(fallbackSubmission.attachments, {
519
+ stage: "upload-fallback",
520
+ subject: "The inline prompt was too large, but its upload fallback",
521
+ });
494
522
  await prepareFallbackSubmission();
495
523
  currentPrompt = fallbackSubmission.prompt;
496
524
  currentAttachments = fallbackSubmission.attachments;
@@ -541,9 +569,11 @@ async function closeRemoteConnectionAfterRun(options) {
541
569
  }
542
570
  }
543
571
  function shouldCloseOwnedRunTargetAfterRun(options) {
544
- return (options.runStatus === "complete" &&
545
- options.ownsTarget &&
546
- (Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser));
572
+ return (options.ownsTarget &&
573
+ ((options.runStatus === "cancelled" &&
574
+ (options.closeOwnedTabOnCancel ?? !options.keepBrowser)) ||
575
+ (options.runStatus === "complete" &&
576
+ (Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser))));
547
577
  }
548
578
  function shouldCleanupBlankTabsAfterLastLease(options) {
549
579
  return (options.runStatus === "complete" &&
@@ -553,6 +583,79 @@ function shouldCleanupBlankTabsAfterLastLease(options) {
553
583
  options.keepBrowser &&
554
584
  Boolean(options.chromePort));
555
585
  }
586
+ async function releaseLocalBrowserTabLease(options) {
587
+ let decisionObserved = false;
588
+ let keepBrowserOpen = false;
589
+ let terminationHandled = false;
590
+ let otherLeasesRemain = false;
591
+ let releaseError;
592
+ try {
593
+ await options.lease.release({
594
+ onRelease: async ({ isLastLease }) => {
595
+ decisionObserved = true;
596
+ if (!isLastLease) {
597
+ // Record this before any best-effort tab cleanup so a cleanup failure can
598
+ // never fall through into terminating Chrome used by another lease.
599
+ keepBrowserOpen = true;
600
+ otherLeasesRemain = true;
601
+ }
602
+ await options.closeOwnedRunTarget().catch(() => undefined);
603
+ if (!isLastLease) {
604
+ return;
605
+ }
606
+ await options.cleanupBlankTabs().catch(() => undefined);
607
+ if (options.terminateSharedChrome) {
608
+ options.logger(`[browser] ChatGPT browser slot ${options.lease.id.slice(0, 8)} is final; ` +
609
+ `terminating shared Chrome (${formatBrowserLeaseDiagnostics(options)}).`);
610
+ const terminated = await options.terminateSharedChrome().catch(() => false);
611
+ if (terminated) {
612
+ terminationHandled = true;
613
+ }
614
+ else {
615
+ // A reused Chrome handle may have a no-op kill implementation. Never
616
+ // claim cleanup or fall through into an unverified lock-free kill.
617
+ keepBrowserOpen = true;
618
+ options.logger("[browser] Could not verify shared Chrome termination; leaving it available for reuse.");
619
+ }
620
+ }
621
+ },
622
+ });
623
+ }
624
+ catch (error) {
625
+ releaseError = error instanceof Error ? error : new Error(String(error));
626
+ if (!terminationHandled)
627
+ keepBrowserOpen = true;
628
+ options.logger(`[browser] Failed to release the ChatGPT browser slot registry lock; restart Oracle/Codex MCP before another browser run: ${releaseError.message}`);
629
+ }
630
+ if (!decisionObserved) {
631
+ options.logger("[browser] Could not verify final ChatGPT tab lease; leaving shared Chrome running.");
632
+ return {
633
+ keepBrowserOpen: true,
634
+ terminationHandled: false,
635
+ ...(releaseError ? { releaseError } : {}),
636
+ };
637
+ }
638
+ if (otherLeasesRemain) {
639
+ options.logger(`[browser] Other ChatGPT tab leases still active; leaving shared Chrome running; ` +
640
+ `browser slot ${options.lease.id.slice(0, 8)} is non-final ` +
641
+ `(${formatBrowserLeaseDiagnostics(options)}).`);
642
+ }
643
+ return {
644
+ keepBrowserOpen,
645
+ terminationHandled,
646
+ ...(releaseError ? { releaseError } : {}),
647
+ };
648
+ }
649
+ function formatBrowserLeaseDiagnostics(options) {
650
+ return [
651
+ `session=${options.sessionId ?? "unknown"}`,
652
+ `controllerPid=${process.pid}`,
653
+ `chromePid=${options.chromePid ?? "unknown"}`,
654
+ `chromePort=${options.chromePort ?? "unknown"}`,
655
+ `target=${options.chromeTargetId ?? "unknown"}`,
656
+ `launch=${options.launchDisposition ?? "unknown"}`,
657
+ ].join("; ");
658
+ }
556
659
  function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
557
660
  return {
558
661
  requestedModel: desiredModel ?? null,
@@ -565,11 +668,25 @@ function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
565
668
  };
566
669
  }
567
670
  export async function runBrowserMode(options) {
671
+ const cancellation = new BrowserCancellation(options.signal, options.log);
672
+ try {
673
+ cancellation.check();
674
+ return await cancellation.run(() => runBrowserModeInternal(options, cancellation));
675
+ }
676
+ finally {
677
+ cancellation.dispose();
678
+ }
679
+ }
680
+ async function runBrowserModeInternal(options, cancellation) {
681
+ const attachments = options.attachments ?? [];
682
+ assertUniqueAttachmentBasenames(attachments, {
683
+ stage: "upload",
684
+ subject: "Browser upload",
685
+ });
568
686
  const promptText = options.prompt?.trim();
569
687
  if (!promptText) {
570
688
  throw new Error("Prompt text is required when using browser mode.");
571
689
  }
572
- const attachments = options.attachments ?? [];
573
690
  const fallbackSubmission = options.fallbackSubmission;
574
691
  let config = resolveBrowserConfig(options.config);
575
692
  const usingCopiedProfile = Boolean(config.copyProfileSource);
@@ -596,6 +713,7 @@ export async function runBrowserMode(options) {
596
713
  let lastUrl;
597
714
  let promptSubmitted = false;
598
715
  let modelSelectionEvidence;
716
+ let thinkingSelectionEvidence;
599
717
  let tabLease = null;
600
718
  let conversationUrlMonitor = null;
601
719
  const emitRuntimeHint = async () => {
@@ -646,7 +764,7 @@ export async function runBrowserMode(options) {
646
764
  logger(line);
647
765
  }
648
766
  if (config.attachRunning) {
649
- const attached = await resolveAttachRunningConnection(config, logger);
767
+ const attached = await cancellation.call(() => resolveAttachRunningConnection(config, logger));
650
768
  config = {
651
769
  ...config,
652
770
  remoteChrome: { host: attached.host, port: attached.port },
@@ -656,7 +774,7 @@ export async function runBrowserMode(options) {
656
774
  }
657
775
  if (!config.remoteChrome && !config.manualLogin) {
658
776
  const preferredPort = config.debugPort ?? DEFAULT_DEBUG_PORT;
659
- const availablePort = await pickAvailableDebugPort(preferredPort, logger);
777
+ const availablePort = await cancellation.call(() => pickAvailableDebugPort(preferredPort, logger));
660
778
  if (availablePort !== preferredPort) {
661
779
  logger(`DevTools port ${preferredPort} busy; using ${availablePort} to avoid attaching to stray Chrome.`);
662
780
  }
@@ -669,7 +787,7 @@ export async function runBrowserMode(options) {
669
787
  if (ignoredFlags.length > 0) {
670
788
  logger(`Note: --remote-chrome ignores local Chrome flags (${ignoredFlags.join(", ")}).`);
671
789
  }
672
- return runRemoteBrowserMode(promptText, attachments, config, logger, options);
790
+ return runRemoteBrowserMode(promptText, attachments, config, logger, options, cancellation);
673
791
  }
674
792
  const manualLogin = Boolean(config.manualLogin);
675
793
  if (manualLogin && usingCopiedProfile) {
@@ -683,70 +801,82 @@ export async function runBrowserMode(options) {
683
801
  : defaultManualLoginProfileDir();
684
802
  const userDataDir = manualLogin
685
803
  ? manualProfileDir
686
- : await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
804
+ : await cancellation.acquire(async () => mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-")), (dir) => rm(dir, { recursive: true, force: true }));
687
805
  const effectiveKeepBrowser = Boolean(config.keepBrowser);
688
- if (manualLogin) {
689
- // Learned: manual login reuses a persistent profile so cookies/SSO survive.
690
- await mkdir(userDataDir, { recursive: true });
691
- logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
692
- await assertManualLoginProfileReadyForRun({
693
- userDataDir,
694
- keepBrowser: effectiveKeepBrowser,
695
- });
696
- }
697
- else if (config.copyProfileSource) {
698
- const copiedProfileDirectory = await copyChromeProfile(config.copyProfileSource, userDataDir, config.chromeProfile);
699
- config = { ...config, chromeProfile: copiedProfileDirectory };
700
- logger(`Seeded temporary Chrome profile ${copiedProfileDirectory} from ${config.copyProfileSource} (copy-profile mode; signed-in session reused without manual login)`);
806
+ try {
807
+ if (manualLogin) {
808
+ // Learned: manual login reuses a persistent profile so cookies/SSO survive.
809
+ await cancellation.call(() => mkdir(userDataDir, { recursive: true }));
810
+ logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
811
+ await cancellation.call(() => assertManualLoginProfileReadyForRun({ userDataDir, keepBrowser: effectiveKeepBrowser }));
812
+ }
813
+ else if (config.copyProfileSource) {
814
+ const copying = copyChromeProfile(config.copyProfileSource, userDataDir, config.chromeProfile);
815
+ const copiedProfileDirectory = await cancellation.race(copying.finally(async () => {
816
+ if (options.signal?.aborted)
817
+ await withoutBrowserCancellation(() => rm(userDataDir, { recursive: true, force: true }));
818
+ }));
819
+ config = { ...config, chromeProfile: copiedProfileDirectory };
820
+ logger(`Seeded temporary Chrome profile ${copiedProfileDirectory} from ${config.copyProfileSource} (copy-profile mode; signed-in session reused without manual login)`);
821
+ }
822
+ else {
823
+ logger(`Created temporary Chrome profile at ${userDataDir}`);
824
+ }
701
825
  }
702
- else {
703
- logger(`Created temporary Chrome profile at ${userDataDir}`);
826
+ catch (error) {
827
+ if (!manualLogin)
828
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
829
+ throw error;
704
830
  }
705
831
  if (manualLogin) {
706
- tabLease = await acquireBrowserTabLease(userDataDir, {
832
+ tabLease = await cancellation.acquire(() => acquireBrowserTabLease(userDataDir, {
707
833
  maxConcurrentTabs: config.maxConcurrentTabs,
708
834
  timeoutMs: config.timeoutMs,
709
835
  logger,
710
836
  sessionId: options.sessionId,
711
- });
837
+ signal: options.signal,
838
+ }), (lease) => lease.release());
712
839
  }
713
840
  let acquiredChrome;
714
841
  try {
715
- acquiredChrome = manualLogin
716
- ? await acquireManualLoginChromeForRun(userDataDir, config, logger, options.sessionId)
717
- : {
718
- chrome: await launchChrome({
719
- ...config,
720
- remoteChrome: config.remoteChrome,
721
- }, userDataDir, logger),
722
- reusedChrome: null,
723
- };
724
- }
725
- catch (error) {
726
- if (tabLease) {
727
- const handle = tabLease;
728
- tabLease = null;
729
- await handle.release().catch(() => undefined);
842
+ if (manualLogin) {
843
+ acquiredChrome = await cancellation.acquire(() => acquireManualLoginChromeForRun(userDataDir, config, logger, options.sessionId), async ({ chrome }) => {
844
+ detachKeptChromeProcess(chrome);
845
+ });
730
846
  }
731
- if (usingCopiedProfile) {
732
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
847
+ else {
848
+ const launched = await cancellation.acquire(() => launchChrome({ ...config, remoteChrome: config.remoteChrome }, userDataDir, logger), async (chrome) => {
849
+ try {
850
+ await chrome.kill();
851
+ }
852
+ finally {
853
+ await rm(userDataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
854
+ }
855
+ });
856
+ acquiredChrome = { chrome: launched, reusedChrome: null };
733
857
  }
858
+ }
859
+ catch (error) {
860
+ await withoutBrowserCancellation(async () => {
861
+ if (tabLease) {
862
+ const handle = tabLease;
863
+ tabLease = null;
864
+ await handle.release().catch(() => undefined);
865
+ }
866
+ if (!manualLogin)
867
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
868
+ });
734
869
  throw error;
735
870
  }
736
871
  const { chrome, reusedChrome } = acquiredChrome;
737
872
  const chromeHost = chrome.host ?? "127.0.0.1";
738
- if (tabLease) {
739
- await tabLease.update({
740
- chromeHost,
741
- chromePort: chrome.port,
742
- });
743
- }
744
873
  let removeTerminationHooks = null;
745
874
  try {
746
875
  removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
747
876
  isInFlight: () => runStatus !== "complete",
748
877
  emitRuntimeHint,
749
878
  preserveUserDataDir: manualLogin,
879
+ preserveSharedChromeOnSignal: manualLogin,
750
880
  // copy-profile is a throwaway copy of a signed-in profile; never leave it on disk.
751
881
  forceProfileCleanup: usingCopiedProfile,
752
882
  });
@@ -768,14 +898,17 @@ export async function runBrowserMode(options) {
768
898
  let appliedCookies = 0;
769
899
  let preserveBrowserOnError = false;
770
900
  try {
901
+ if (tabLease)
902
+ await cancellation.call(() => tabLease.update({ chromeHost, chromePort: chrome.port }));
771
903
  try {
772
904
  if (config.browserTabRef) {
773
- const attached = await connectToExistingChatGptTab({
905
+ const tabRef = config.browserTabRef;
906
+ const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
774
907
  host: chromeHost,
775
908
  port: chrome.port,
776
- ref: config.browserTabRef,
777
- });
778
- client = attached.client;
909
+ ref: tabRef,
910
+ }), (attached) => attached.client.close());
911
+ client = cancellation.client(attached.client);
779
912
  isolatedTargetId = attached.targetId ?? null;
780
913
  lastTargetId = attached.targetId ?? undefined;
781
914
  lastUrl = attached.tab.url || lastUrl;
@@ -785,12 +918,16 @@ export async function runBrowserMode(options) {
785
918
  else {
786
919
  const strictTabIsolation = Boolean(manualLogin && reusedChrome);
787
920
  const devtoolsRetries = manualLogin ? 6 : 0;
788
- const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
921
+ const connection = await cancellation.acquire(() => connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
789
922
  fallbackToDefault: !strictTabIsolation,
790
923
  retries: devtoolsRetries,
791
924
  retryDelayMs: 500,
925
+ }), async (connection) => {
926
+ await connection.client.close().catch(() => undefined);
927
+ if (connection.targetId)
928
+ await closeTab(chrome.port, connection.targetId, logger, chromeHost);
792
929
  });
793
- client = connection.client;
930
+ client = cancellation.client(connection.client);
794
931
  isolatedTargetId = connection.targetId ?? null;
795
932
  ownsTarget = true;
796
933
  }
@@ -846,7 +983,7 @@ export async function runBrowserMode(options) {
846
983
  })();
847
984
  });
848
985
  });
849
- const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
986
+ const raceWithDisconnect = (promise) => cancellation.race(Promise.race([promise, disconnectPromise]));
850
987
  const { Network, Page, Runtime, Input, DOM, Target } = client;
851
988
  const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
852
989
  if (DOM && typeof DOM.enable === "function") {
@@ -854,7 +991,12 @@ export async function runBrowserMode(options) {
854
991
  }
855
992
  await Promise.all(domainEnablers);
856
993
  if (!config.headless && config.hideWindow) {
857
- await positionChromeWindowOffscreen(client, logger);
994
+ await positionChromeWindowOffscreen(client, userDataDir, logger);
995
+ }
996
+ else if (!config.headless) {
997
+ // Persistent profiles can retain bounds from a prior hidden run. Visible
998
+ // local runs must actively restore the Oracle-owned Chrome window.
999
+ await positionChromeWindowOnscreen(client, userDataDir, logger);
858
1000
  }
859
1001
  // The send button is clicked with trusted CDP input events at viewport
860
1002
  // coordinates, which ChatGPT silently drops when the window is hidden or
@@ -1076,16 +1218,14 @@ export async function runBrowserMode(options) {
1076
1218
  : "Model picker: skipped (strategy=ignore)");
1077
1219
  }
1078
1220
  const deepResearch = config.researchMode === "deep";
1079
- // Handle thinking time selection if specified. Deep Research owns its own effort flow.
1080
- const thinkingTime = config.thinkingTime;
1081
- if (thinkingTime && !deepResearch) {
1221
+ if (shouldApplyThinkingTimeSelection(config)) {
1082
1222
  const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
1083
- await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
1223
+ thinkingSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, config.thinkingTime, logger, thinkingTargetModel), {
1084
1224
  retries: 2,
1085
1225
  delayMs: 300,
1086
1226
  onRetry: (attempt, error) => {
1087
1227
  if (options.verbose) {
1088
- logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
1228
+ logger(`[retry] Thinking time (${config.thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
1089
1229
  }
1090
1230
  },
1091
1231
  }));
@@ -1095,9 +1235,12 @@ export async function runBrowserMode(options) {
1095
1235
  const acquireProfileLockIfNeeded = async () => {
1096
1236
  if (profileLockTimeoutMs <= 0)
1097
1237
  return;
1098
- profileLock = await acquireProfileRunLock(userDataDir, {
1238
+ profileLock = await cancellation.acquire(() => acquireProfileRunLock(userDataDir, {
1099
1239
  timeoutMs: profileLockTimeoutMs,
1100
1240
  logger,
1241
+ signal: options.signal,
1242
+ }), async (lock) => {
1243
+ await lock?.release();
1101
1244
  });
1102
1245
  };
1103
1246
  const releaseProfileLockIfHeld = async () => {
@@ -1105,7 +1248,7 @@ export async function runBrowserMode(options) {
1105
1248
  return;
1106
1249
  const handle = profileLock;
1107
1250
  profileLock = null;
1108
- await handle.release().catch(() => undefined);
1251
+ await withoutBrowserCancellation(() => handle.release()).catch(() => undefined);
1109
1252
  };
1110
1253
  const submitOnce = async (prompt, submissionAttachments) => {
1111
1254
  const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
@@ -1116,17 +1259,20 @@ export async function runBrowserMode(options) {
1116
1259
  generatedBundle: a.generatedBundle === true,
1117
1260
  }));
1118
1261
  let inputOnlyAttachments = false;
1262
+ let attachmentNavigationUrl;
1119
1263
  await raceWithDisconnect(clearPromptComposer(Runtime, logger));
1120
1264
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
1121
1265
  if (submissionAttachments.length > 0) {
1122
1266
  if (!DOM) {
1123
1267
  throw new Error("Chrome DOM domain unavailable while uploading attachments.");
1124
1268
  }
1269
+ attachmentNavigationUrl = await raceWithDisconnect(captureComposerNavigationUrl(Runtime));
1125
1270
  await clearComposerAttachments(Runtime, 5_000, logger);
1126
1271
  for (let attachmentIndex = 0; attachmentIndex < submissionAttachments.length; attachmentIndex += 1) {
1127
1272
  const attachment = submissionAttachments[attachmentIndex];
1273
+ await raceWithDisconnect(assertComposerPlusStayedInPlace(Runtime, attachmentNavigationUrl));
1128
1274
  logger(`Uploading attachment: ${attachment.displayPath}`);
1129
- const uiConfirmed = await uploadAttachmentFile({ runtime: Runtime, dom: DOM, input: Input }, attachment, logger, { expectedCount: attachmentIndex + 1 });
1275
+ const uiConfirmed = await uploadAttachmentFile({ runtime: Runtime, dom: DOM, input: Input }, attachment, logger, { expectedCount: attachmentIndex + 1, navigationUrl: attachmentNavigationUrl });
1130
1276
  if (!uiConfirmed) {
1131
1277
  inputOnlyAttachments = true;
1132
1278
  }
@@ -1158,12 +1304,14 @@ export async function runBrowserMode(options) {
1158
1304
  const providerState = {
1159
1305
  runtime: Runtime,
1160
1306
  input: Input,
1307
+ page: Page,
1161
1308
  logger,
1162
1309
  timeoutMs: config.timeoutMs,
1163
1310
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
1164
1311
  attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
1165
1312
  baselineTurns: baselineTurns ?? undefined,
1166
1313
  attachmentNames: attachmentExpectations,
1314
+ attachmentNavigationUrl,
1167
1315
  onPromptSubmitted: markPromptSubmitted,
1168
1316
  };
1169
1317
  const deepResearchTargetBaseline = deepResearch && client
@@ -1278,6 +1426,7 @@ export async function runBrowserMode(options) {
1278
1426
  artifacts: savedArtifacts,
1279
1427
  archive,
1280
1428
  modelSelection: modelSelectionEvidence,
1429
+ thinkingSelection: thinkingSelectionEvidence,
1281
1430
  tookMs: durationMs,
1282
1431
  answerTokens: tokens,
1283
1432
  answerChars: researchResult.text.length,
@@ -1302,6 +1451,7 @@ export async function runBrowserMode(options) {
1302
1451
  const deadline = Date.now() + timeoutMs;
1303
1452
  while (Date.now() < deadline) {
1304
1453
  const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
1454
+ throwIfAssistantUiError(snapshot);
1305
1455
  const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
1306
1456
  if (text) {
1307
1457
  const normalized = normalizeForComparison(text);
@@ -1529,6 +1679,7 @@ export async function runBrowserMode(options) {
1529
1679
  let stableCount = 0;
1530
1680
  while (Date.now() < deadline) {
1531
1681
  const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
1682
+ throwIfAssistantUiError(snapshot);
1532
1683
  const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
1533
1684
  const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
1534
1685
  if (!isStillEcho) {
@@ -1558,6 +1709,7 @@ export async function runBrowserMode(options) {
1558
1709
  let stableCycles = 0;
1559
1710
  while (Date.now() < deadline) {
1560
1711
  const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
1712
+ throwIfAssistantUiError(snapshot);
1561
1713
  const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
1562
1714
  if (text && text.length > bestText.length) {
1563
1715
  bestText = text;
@@ -1712,6 +1864,7 @@ export async function runBrowserMode(options) {
1712
1864
  savedFiles: fileArtifacts.savedFiles,
1713
1865
  archive,
1714
1866
  modelSelection: modelSelectionEvidence,
1867
+ thinkingSelection: thinkingSelectionEvidence,
1715
1868
  tookMs: durationMs,
1716
1869
  answerTokens,
1717
1870
  answerChars,
@@ -1727,6 +1880,10 @@ export async function runBrowserMode(options) {
1727
1880
  };
1728
1881
  }
1729
1882
  catch (error) {
1883
+ if (options.signal?.aborted || error instanceof BrowserRunCancelledError) {
1884
+ runStatus = "cancelled";
1885
+ throw new BrowserRunCancelledError();
1886
+ }
1730
1887
  const normalizedError = error instanceof Error ? error : new Error(String(error));
1731
1888
  const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
1732
1889
  connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
@@ -1813,150 +1970,154 @@ export async function runBrowserMode(options) {
1813
1970
  }, normalizedError);
1814
1971
  }
1815
1972
  finally {
1816
- await conversationUrlMonitor?.stop();
1817
- try {
1818
- if (!connectionClosedUnexpectedly) {
1819
- await client?.close();
1820
- }
1821
- }
1822
- catch {
1823
- // ignore
1824
- }
1825
- // Close the isolated tab once the response has been fully captured to prevent
1826
- // tab accumulation across repeated runs. Keep the tab open on incomplete runs
1827
- // so reattach can recover the response.
1828
- const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
1829
- runStatus,
1830
- ownsTarget,
1831
- keepBrowser: effectiveKeepBrowser,
1832
- closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
1833
- });
1834
- let keepBrowserOpen = shouldKeepLocalBrowserOpen({
1835
- effectiveKeepBrowser,
1836
- preserveBrowserOnError,
1837
- usingCopiedProfile,
1838
- });
1839
- let cleanupProfileLock = null;
1840
- let terminatedRecordedChrome = false;
1841
- let otherActiveBrowserTabLeases = null;
1842
- const hasOtherActiveLeases = async () => {
1843
- if (!manualLogin || !tabLease) {
1844
- return false;
1845
- }
1846
- if (otherActiveBrowserTabLeases === null) {
1847
- otherActiveBrowserTabLeases = await hasOtherActiveBrowserTabLeases(userDataDir, tabLease.id);
1848
- }
1849
- return otherActiveBrowserTabLeases;
1850
- };
1851
- if (!keepBrowserOpen && manualLogin && tabLease) {
1852
- const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
1853
- if (cleanupLockTimeoutMs > 0) {
1854
- cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
1855
- timeoutMs: cleanupLockTimeoutMs,
1856
- logger,
1857
- sessionId: options.sessionId,
1858
- }).catch(() => null);
1859
- }
1860
- keepBrowserOpen = await hasOtherActiveLeases().catch(() => false);
1861
- if (keepBrowserOpen) {
1862
- logger("[browser] Other ChatGPT tab leases still active; leaving shared Chrome running.");
1863
- }
1864
- else if (reusedChrome && !connectionClosedUnexpectedly) {
1865
- terminatedRecordedChrome = await terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false);
1866
- }
1867
- }
1868
- const closeOwnedRunTarget = async () => {
1869
- if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
1870
- return;
1871
- }
1872
- const safeToClose = !effectiveKeepBrowser ||
1873
- Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
1874
- if (!safeToClose) {
1875
- logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
1876
- return;
1877
- }
1878
- const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
1879
- if (!closeConfirmed && effectiveKeepBrowser) {
1880
- const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
1881
- if (!replacementTargetId) {
1882
- logger(`[browser] Chrome page retention could not be verified after closing ${isolatedTargetId}.`);
1973
+ await withoutBrowserCancellation(async () => {
1974
+ stopThinkingMonitor?.();
1975
+ await conversationUrlMonitor?.stop();
1976
+ try {
1977
+ if (!connectionClosedUnexpectedly) {
1978
+ await client?.close();
1883
1979
  }
1884
1980
  }
1885
- };
1886
- const cleanupBlankTabs = async () => {
1887
- if (!shouldCleanupBlankTabsAfterLastLease({
1981
+ catch {
1982
+ // ignore
1983
+ }
1984
+ // Close the isolated tab once the response has been fully captured to prevent
1985
+ // tab accumulation across repeated runs. Keep the tab open on incomplete runs
1986
+ // so reattach can recover the response.
1987
+ const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
1888
1988
  runStatus,
1889
1989
  ownsTarget,
1890
- connectionClosedUnexpectedly,
1891
- manualLogin,
1892
1990
  keepBrowser: effectiveKeepBrowser,
1893
- chromePort: chrome?.port,
1894
- }) ||
1895
- !chrome?.port) {
1896
- return;
1897
- }
1898
- await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
1899
- excludeTargetIds: [isolatedTargetId, lastTargetId],
1900
- preserveOneBlank: true,
1991
+ closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
1992
+ closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
1901
1993
  });
1902
- };
1903
- if (tabLease) {
1904
- const handle = tabLease;
1905
- tabLease = null;
1906
- const onRelease = async ({ isLastLease }) => {
1907
- await closeOwnedRunTarget();
1908
- if (isLastLease) {
1909
- await cleanupBlankTabs();
1994
+ let keepBrowserOpen = (manualLogin && runStatus === "cancelled") ||
1995
+ shouldKeepLocalBrowserOpen({
1996
+ effectiveKeepBrowser,
1997
+ preserveBrowserOnError,
1998
+ usingCopiedProfile,
1999
+ });
2000
+ let cleanupProfileLock = null;
2001
+ let browserTerminationHandledByLease = false;
2002
+ let tabLeaseReleaseError;
2003
+ if (!keepBrowserOpen && manualLogin && tabLease) {
2004
+ const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
2005
+ if (cleanupLockTimeoutMs > 0) {
2006
+ cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
2007
+ timeoutMs: cleanupLockTimeoutMs,
2008
+ logger,
2009
+ sessionId: options.sessionId,
2010
+ }).catch(() => null);
1910
2011
  }
1911
- };
1912
- await handle.release({ onRelease }).catch(() => undefined);
1913
- }
1914
- else {
1915
- await closeOwnedRunTarget();
1916
- await cleanupBlankTabs();
1917
- }
1918
- removeDialogHandler?.();
1919
- removeTerminationHooks?.();
1920
- if (!keepBrowserOpen) {
1921
- if (!connectionClosedUnexpectedly) {
1922
- try {
1923
- if (!terminatedRecordedChrome) {
1924
- await chrome.kill();
1925
- }
2012
+ }
2013
+ const closeOwnedRunTarget = async () => {
2014
+ if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
2015
+ return;
1926
2016
  }
1927
- catch {
1928
- // ignore kill failures
2017
+ const safeToClose = !keepBrowserOpen ||
2018
+ Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
2019
+ if (!safeToClose) {
2020
+ logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
2021
+ return;
1929
2022
  }
1930
- }
1931
- if (manualLogin) {
1932
- const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, {
2023
+ const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
2024
+ if (!closeConfirmed && keepBrowserOpen) {
2025
+ const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
2026
+ if (!replacementTargetId) {
2027
+ logger(`[browser] Chrome page retention could not be verified after closing ${isolatedTargetId}.`);
2028
+ }
2029
+ }
2030
+ };
2031
+ const cleanupBlankTabs = async () => {
2032
+ if (!shouldCleanupBlankTabsAfterLastLease({
2033
+ runStatus,
2034
+ ownsTarget,
1933
2035
  connectionClosedUnexpectedly,
1934
- host: chromeHost,
2036
+ manualLogin,
2037
+ keepBrowser: effectiveKeepBrowser,
2038
+ chromePort: chrome?.port,
2039
+ }) ||
2040
+ !chrome?.port) {
2041
+ return;
2042
+ }
2043
+ await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
2044
+ excludeTargetIds: [isolatedTargetId, lastTargetId],
2045
+ preserveOneBlank: true,
2046
+ });
2047
+ };
2048
+ if (tabLease) {
2049
+ const handle = tabLease;
2050
+ tabLease = null;
2051
+ const terminateSharedChrome = !keepBrowserOpen && manualLogin && !connectionClosedUnexpectedly
2052
+ ? async () => terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false)
2053
+ : undefined;
2054
+ const releaseResult = await releaseLocalBrowserTabLease({
2055
+ lease: handle,
2056
+ closeOwnedRunTarget,
2057
+ cleanupBlankTabs,
2058
+ terminateSharedChrome,
2059
+ sessionId: options.sessionId,
2060
+ chromePid: chrome.pid,
2061
+ chromePort: chrome.port,
2062
+ chromeTargetId: isolatedTargetId,
2063
+ launchDisposition: reusedChrome ? "reused" : "launched",
2064
+ logger,
1935
2065
  });
1936
- if (shouldCleanup) {
1937
- // Preserve the persistent manual-login profile, but clear stale reattach hints.
1938
- await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
2066
+ keepBrowserOpen ||= releaseResult.keepBrowserOpen;
2067
+ browserTerminationHandledByLease = releaseResult.terminationHandled;
2068
+ tabLeaseReleaseError = releaseResult.releaseError;
2069
+ }
2070
+ else {
2071
+ await closeOwnedRunTarget();
2072
+ await cleanupBlankTabs();
2073
+ }
2074
+ removeDialogHandler?.();
2075
+ removeTerminationHooks?.();
2076
+ if (!keepBrowserOpen) {
2077
+ if (!connectionClosedUnexpectedly) {
2078
+ try {
2079
+ if (!browserTerminationHandledByLease) {
2080
+ await chrome.kill();
2081
+ }
2082
+ }
2083
+ catch {
2084
+ // ignore kill failures
2085
+ }
2086
+ }
2087
+ if (manualLogin) {
2088
+ const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, {
2089
+ connectionClosedUnexpectedly,
2090
+ host: chromeHost,
2091
+ });
2092
+ if (shouldCleanup) {
2093
+ // Preserve the persistent manual-login profile, but clear stale reattach hints.
2094
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
2095
+ }
2096
+ }
2097
+ else {
2098
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
2099
+ }
2100
+ if (!connectionClosedUnexpectedly) {
2101
+ const totalSeconds = (Date.now() - startedAt) / 1000;
2102
+ logger(`Cleanup ${runStatus} • ${totalSeconds.toFixed(1)}s total`);
1939
2103
  }
1940
2104
  }
1941
2105
  else {
1942
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
2106
+ detachKeptChromeProcess(chrome);
2107
+ if (!connectionClosedUnexpectedly) {
2108
+ logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
2109
+ }
1943
2110
  }
1944
- if (!connectionClosedUnexpectedly) {
1945
- const totalSeconds = (Date.now() - startedAt) / 1000;
1946
- logger(`Cleanup ${runStatus} • ${totalSeconds.toFixed(1)}s total`);
2111
+ if (cleanupProfileLock) {
2112
+ const handle = cleanupProfileLock;
2113
+ cleanupProfileLock = null;
2114
+ await handle.release().catch(() => undefined);
1947
2115
  }
1948
- }
1949
- else {
1950
- detachKeptChromeProcess(chrome);
1951
- if (!connectionClosedUnexpectedly) {
1952
- logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
2116
+ if (tabLeaseReleaseError) {
2117
+ // oxlint-disable-next-line eslint/no-unsafe-finally -- This cleanup failure must override a successful browser result or a live MCP owner can remain locked.
2118
+ throw new Error("Failed to release the ChatGPT browser slot registry lock; restart Oracle/Codex MCP before another browser run.", { cause: tabLeaseReleaseError });
1953
2119
  }
1954
- }
1955
- if (cleanupProfileLock) {
1956
- const handle = cleanupProfileLock;
1957
- cleanupProfileLock = null;
1958
- await handle.release().catch(() => undefined);
1959
- }
2120
+ });
1960
2121
  }
1961
2122
  }
1962
2123
  const DEFAULT_DEBUG_PORT = 9222;
@@ -2190,7 +2351,7 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
2190
2351
  process: undefined,
2191
2352
  };
2192
2353
  }
2193
- async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
2354
+ async function runRemoteBrowserMode(promptText, attachments, config, logger, options, cancellation) {
2194
2355
  const remoteChromeConfig = config.remoteChrome;
2195
2356
  if (!remoteChromeConfig) {
2196
2357
  throw new Error("Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.");
@@ -2203,6 +2364,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2203
2364
  let lastUrl;
2204
2365
  let promptSubmitted = false;
2205
2366
  let modelSelectionEvidence;
2367
+ let thinkingSelectionEvidence;
2206
2368
  let attachedExistingTab = false;
2207
2369
  let ownsTarget = true;
2208
2370
  let conversationUrlMonitor = null;
@@ -2259,22 +2421,24 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2259
2421
  : resolveRemoteTabLeaseProfileDir(config);
2260
2422
  if (remoteLeaseProfileDir) {
2261
2423
  await mkdir(remoteLeaseProfileDir, { recursive: true });
2262
- tabLease = await acquireBrowserTabLease(remoteLeaseProfileDir, {
2424
+ tabLease = await cancellation.acquire(() => acquireBrowserTabLease(remoteLeaseProfileDir, {
2263
2425
  maxConcurrentTabs: config.maxConcurrentTabs,
2264
2426
  timeoutMs: config.timeoutMs,
2265
2427
  logger,
2266
2428
  sessionId: options.sessionId,
2267
2429
  chromeHost: host,
2268
2430
  chromePort: port,
2269
- });
2431
+ signal: options.signal,
2432
+ }), (lease) => lease.release());
2270
2433
  }
2271
2434
  if (config.browserTabRef) {
2272
- const attached = await connectToExistingChatGptTab({
2435
+ const tabRef = config.browserTabRef;
2436
+ const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
2273
2437
  host,
2274
2438
  port,
2275
- ref: config.browserTabRef,
2276
- });
2277
- client = attached.client;
2439
+ ref: tabRef,
2440
+ }), (attached) => attached.client.close());
2441
+ client = cancellation.client(attached.client);
2278
2442
  remoteTargetId = attached.targetId ?? null;
2279
2443
  lastUrl = attached.tab.url || lastUrl;
2280
2444
  attachedExistingTab = true;
@@ -2282,10 +2446,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2282
2446
  logger(`Attached to existing remote ChatGPT tab ${attached.targetId}${attached.tab.url ? ` (${attached.tab.url})` : ""}`);
2283
2447
  }
2284
2448
  else {
2285
- connection = await connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
2286
- approvalWaitMs: config.attachRunning && browserWSEndpoint ? 20_000 : undefined,
2287
- });
2288
- client = connection.client;
2449
+ connection = await cancellation.acquire(() => connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
2450
+ approvalWaitMs: browserWSEndpoint ? config.approvalWaitMs : undefined,
2451
+ fallbackToDefault: false,
2452
+ }), (connection) => connection.close());
2453
+ client = cancellation.client(connection.client);
2289
2454
  remoteTargetId = connection.targetId ?? null;
2290
2455
  ownsTarget = true;
2291
2456
  }
@@ -2394,16 +2559,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2394
2559
  : "Model picker: skipped (strategy=ignore)");
2395
2560
  }
2396
2561
  const deepResearch = config.researchMode === "deep";
2397
- // Handle thinking time selection if specified. Deep Research owns its own effort flow.
2398
- const thinkingTime = config.thinkingTime;
2399
- if (thinkingTime && !deepResearch) {
2562
+ if (shouldApplyThinkingTimeSelection(config)) {
2400
2563
  const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
2401
- await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
2564
+ thinkingSelectionEvidence = await withRetries(() => ensureThinkingTime(Runtime, config.thinkingTime, logger, thinkingTargetModel), {
2402
2565
  retries: 2,
2403
2566
  delayMs: 300,
2404
2567
  onRetry: (attempt, error) => {
2405
2568
  if (options.verbose) {
2406
- logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
2569
+ logger(`[retry] Thinking time (${config.thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
2407
2570
  }
2408
2571
  },
2409
2572
  });
@@ -2416,17 +2579,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2416
2579
  name: path.basename(a.path),
2417
2580
  generatedBundle: a.generatedBundle === true,
2418
2581
  }));
2582
+ let attachmentNavigationUrl;
2419
2583
  await clearPromptComposer(Runtime, logger);
2420
2584
  await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
2421
2585
  if (submissionAttachments.length > 0) {
2422
2586
  if (!DOM) {
2423
2587
  throw new Error("Chrome DOM domain unavailable while uploading attachments.");
2424
2588
  }
2589
+ attachmentNavigationUrl = await captureComposerNavigationUrl(Runtime);
2425
2590
  await clearComposerAttachments(Runtime, 5_000, logger);
2426
2591
  // Use remote file transfer for remote Chrome (reads local files and injects via CDP)
2427
2592
  for (const attachment of submissionAttachments) {
2593
+ await assertComposerPlusStayedInPlace(Runtime, attachmentNavigationUrl);
2428
2594
  logger(`Uploading attachment: ${attachment.displayPath}`);
2429
- await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
2595
+ await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM, navigationUrl: attachmentNavigationUrl }, attachment, logger);
2430
2596
  await delay(500);
2431
2597
  }
2432
2598
  // Scale timeout based on number of files: base 30s + 15s per additional file
@@ -2454,12 +2620,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2454
2620
  const providerState = {
2455
2621
  runtime: Runtime,
2456
2622
  input: Input,
2623
+ page: Page,
2457
2624
  logger,
2458
2625
  timeoutMs: config.timeoutMs,
2459
2626
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
2460
2627
  attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
2461
2628
  baselineTurns: baselineTurns ?? undefined,
2462
2629
  attachmentNames: attachmentExpectations,
2630
+ attachmentNavigationUrl,
2463
2631
  onPromptSubmitted: markPromptSubmitted,
2464
2632
  };
2465
2633
  const deepResearchTargetBaseline = deepResearch && client
@@ -2550,6 +2718,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2550
2718
  artifacts: savedArtifacts,
2551
2719
  archive,
2552
2720
  modelSelection: modelSelectionEvidence,
2721
+ thinkingSelection: thinkingSelectionEvidence,
2553
2722
  tookMs: durationMs,
2554
2723
  answerTokens: tokens,
2555
2724
  answerChars: researchResult.text.length,
@@ -2572,6 +2741,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2572
2741
  const deadline = Date.now() + timeoutMs;
2573
2742
  while (Date.now() < deadline) {
2574
2743
  const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
2744
+ throwIfAssistantUiError(snapshot);
2575
2745
  const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
2576
2746
  if (text) {
2577
2747
  const normalized = normalizeForComparison(text);
@@ -2793,6 +2963,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2793
2963
  let stableCount = 0;
2794
2964
  while (Date.now() < deadline) {
2795
2965
  const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
2966
+ throwIfAssistantUiError(snapshot);
2796
2967
  const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
2797
2968
  const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
2798
2969
  if (!isStillEcho) {
@@ -2956,10 +3127,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2956
3127
  savedFiles: fileArtifacts.savedFiles,
2957
3128
  archive,
2958
3129
  modelSelection: modelSelectionEvidence,
3130
+ thinkingSelection: thinkingSelectionEvidence,
2959
3131
  controllerPid: process.pid,
2960
3132
  };
2961
3133
  }
2962
3134
  catch (error) {
3135
+ if (options.signal?.aborted || error instanceof BrowserRunCancelledError) {
3136
+ runStatus = "cancelled";
3137
+ throw new BrowserRunCancelledError();
3138
+ }
2963
3139
  const normalizedError = error instanceof Error ? error : new Error(String(error));
2964
3140
  const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
2965
3141
  connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
@@ -2997,57 +3173,61 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2997
3173
  });
2998
3174
  }
2999
3175
  finally {
3000
- await conversationUrlMonitor?.stop();
3001
- try {
3002
- await closeRemoteConnectionAfterRun({
3003
- connectionClosedUnexpectedly,
3004
- connection,
3005
- client,
3006
- runStatus,
3007
- });
3008
- }
3009
- catch {
3010
- // ignore
3011
- }
3012
- removeDialogHandler?.();
3013
- const keepRemoteBrowser = Boolean(config.keepBrowser);
3014
- const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
3015
- runStatus,
3016
- ownsTarget,
3017
- keepBrowser: keepRemoteBrowser,
3018
- closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
3019
- });
3020
- const closeOwnedRemoteTarget = async () => {
3021
- if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
3022
- return;
3176
+ await withoutBrowserCancellation(async () => {
3177
+ stopThinkingMonitor?.();
3178
+ await conversationUrlMonitor?.stop();
3179
+ try {
3180
+ await closeRemoteConnectionAfterRun({
3181
+ connectionClosedUnexpectedly,
3182
+ connection,
3183
+ client,
3184
+ runStatus,
3185
+ });
3023
3186
  }
3024
- const safeToClose = !keepRemoteBrowser ||
3025
- Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
3026
- if (!safeToClose) {
3027
- logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
3028
- return;
3187
+ catch {
3188
+ // ignore
3029
3189
  }
3030
- const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
3031
- if (!closeConfirmed && keepRemoteBrowser) {
3032
- const replacementTargetId = await createChromePageTarget(port, logger, host);
3033
- if (!replacementTargetId) {
3034
- logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
3190
+ removeDialogHandler?.();
3191
+ const keepRemoteBrowser = Boolean(config.keepBrowser);
3192
+ const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
3193
+ runStatus,
3194
+ ownsTarget,
3195
+ keepBrowser: keepRemoteBrowser,
3196
+ closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
3197
+ closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
3198
+ });
3199
+ const closeOwnedRemoteTarget = async () => {
3200
+ if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
3201
+ return;
3035
3202
  }
3203
+ const safeToClose = !keepRemoteBrowser ||
3204
+ Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
3205
+ if (!safeToClose) {
3206
+ logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
3207
+ return;
3208
+ }
3209
+ const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
3210
+ if (!closeConfirmed && keepRemoteBrowser) {
3211
+ const replacementTargetId = await createChromePageTarget(port, logger, host);
3212
+ if (!replacementTargetId) {
3213
+ logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
3214
+ }
3215
+ }
3216
+ };
3217
+ if (tabLease) {
3218
+ const handle = tabLease;
3219
+ tabLease = null;
3220
+ await handle
3221
+ .release({ onRelease: async () => closeOwnedRemoteTarget() })
3222
+ .catch(() => undefined);
3036
3223
  }
3037
- };
3038
- if (tabLease) {
3039
- const handle = tabLease;
3040
- tabLease = null;
3041
- await handle
3042
- .release({ onRelease: async () => closeOwnedRemoteTarget() })
3043
- .catch(() => undefined);
3044
- }
3045
- else {
3046
- await closeOwnedRemoteTarget();
3047
- }
3048
- // Don't kill remote Chrome - it's not ours to manage
3049
- const totalSeconds = (Date.now() - startedAt) / 1000;
3050
- logger(`Remote session complete • ${totalSeconds.toFixed(1)}s total`);
3224
+ else {
3225
+ await closeOwnedRemoteTarget();
3226
+ }
3227
+ // Don't kill remote Chrome - it's not ours to manage
3228
+ const totalSeconds = (Date.now() - startedAt) / 1000;
3229
+ logger(`Remote session complete ${totalSeconds.toFixed(1)}s total`);
3230
+ });
3051
3231
  }
3052
3232
  }
3053
3233
  export { estimateTokenCount } from "./utils.js";
@@ -3066,10 +3246,13 @@ export const __test__ = {
3066
3246
  isImageOnlyUiChromeText,
3067
3247
  listIgnoredRemoteChromeFlags,
3068
3248
  normalizeAuthenticatedModelSelectionError,
3249
+ pollGeneratedImageOrTextAssistantResponse,
3069
3250
  resolveManualLoginWaitMs,
3251
+ shouldApplyThinkingTimeSelection,
3070
3252
  shouldCleanupBlankTabsAfterLastLease,
3071
3253
  shouldCloseOwnedRunTargetAfterRun,
3072
3254
  shouldKeepLocalBrowserOpen,
3255
+ releaseLocalBrowserTabLease,
3073
3256
  waitForAssistantResponseWithReload,
3074
3257
  };
3075
3258
  export { syncCookies } from "./cookies.js";