@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.
- package/README.md +1 -1
- package/dist/bin/oracle-cli.js +65 -63
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +2 -1
- package/dist/src/browser/actions/modelSelection.js +13 -3
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +361 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +119 -16
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +267 -43
- package/dist/src/browser/config.js +13 -0
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +444 -261
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +10 -3
- package/dist/src/browser/sessionRunner.js +71 -11
- package/dist/src/browser/tabLeaseRegistry.js +414 -38
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +3 -1
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +9 -2
- package/dist/src/cli/sessionRunner.js +91 -7
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +84 -22
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +7 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/remote/client.js +70 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +345 -71
- package/dist/src/sessionManager.js +36 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +25 -23
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -683
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -400
- package/dist/docs-site/browser-mode.html +0 -588
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -595
- package/dist/docs-site/mcp.html +0 -368
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -387
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- 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 {
|
|
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
|
|
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 (
|
|
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.
|
|
545
|
-
options.
|
|
546
|
-
|
|
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
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
userDataDir,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
703
|
-
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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
|
-
|
|
732
|
-
await
|
|
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
|
|
905
|
+
const tabRef = config.browserTabRef;
|
|
906
|
+
const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
|
|
774
907
|
host: chromeHost,
|
|
775
908
|
port: chrome.port,
|
|
776
|
-
ref:
|
|
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
|
-
|
|
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
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
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
|
-
|
|
1887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
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
|
-
|
|
1928
|
-
|
|
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
|
-
|
|
1932
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
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
|
-
|
|
2106
|
+
detachKeptChromeProcess(chrome);
|
|
2107
|
+
if (!connectionClosedUnexpectedly) {
|
|
2108
|
+
logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
|
|
2109
|
+
}
|
|
1943
2110
|
}
|
|
1944
|
-
if (
|
|
1945
|
-
const
|
|
1946
|
-
|
|
2111
|
+
if (cleanupProfileLock) {
|
|
2112
|
+
const handle = cleanupProfileLock;
|
|
2113
|
+
cleanupProfileLock = null;
|
|
2114
|
+
await handle.release().catch(() => undefined);
|
|
1947
2115
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
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
|
|
2435
|
+
const tabRef = config.browserTabRef;
|
|
2436
|
+
const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
|
|
2273
2437
|
host,
|
|
2274
2438
|
port,
|
|
2275
|
-
ref:
|
|
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:
|
|
2287
|
-
|
|
2288
|
-
|
|
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
|
-
|
|
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
|
|
3001
|
-
|
|
3002
|
-
await
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
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
|
-
|
|
3025
|
-
|
|
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
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
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";
|