@steipete/oracle 0.17.3 → 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 +75 -64
- 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 +267 -15
- 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 +15 -3
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +447 -263
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/policies.js +6 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +88 -17
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
- package/dist/src/browser/reattach.js +14 -4
- 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 +40 -15
- package/dist/src/cli/browserDefaults.js +13 -0
- package/dist/src/cli/browserTabs.js +12 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -47
- package/dist/src/cli/harvestIntegrity.js +140 -0
- package/dist/src/cli/projectSources.js +5 -3
- 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 +97 -26
- 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 +351 -72
- 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";
|
|
@@ -32,7 +35,7 @@ import { captureBrowserDiagnostics } from "./domDebug.js";
|
|
|
32
35
|
import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./actions/archiveConversation.js";
|
|
33
36
|
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
34
37
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
|
|
35
|
-
import { shouldSyncBrowserCookies } from "./policies.js";
|
|
38
|
+
import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
|
|
36
39
|
import { createConversationUrlMonitor, } from "./conversationUrlMonitor.js";
|
|
37
40
|
import { extractStableConversationIdFromUrl as extractConversationIdFromUrl, isStableConversationUrl as isConversationUrl, } from "./conversationUrl.js";
|
|
38
41
|
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.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
|
|
@@ -874,6 +1016,7 @@ export async function runBrowserMode(options) {
|
|
|
874
1016
|
logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
|
|
875
1017
|
}
|
|
876
1018
|
if (!config.inlineCookies) {
|
|
1019
|
+
logger(CHROME_COOKIE_SYNC_WARNING);
|
|
877
1020
|
logger("Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.");
|
|
878
1021
|
}
|
|
879
1022
|
else {
|
|
@@ -902,7 +1045,7 @@ export async function runBrowserMode(options) {
|
|
|
902
1045
|
else {
|
|
903
1046
|
logger(manualLogin
|
|
904
1047
|
? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
|
|
905
|
-
: "Skipping Chrome cookie
|
|
1048
|
+
: "Skipping Chrome cookie copy (disabled by default; use --browser-cookie-sync to opt in).");
|
|
906
1049
|
}
|
|
907
1050
|
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
908
1051
|
preserveConversationIds: [
|
|
@@ -1075,16 +1218,14 @@ export async function runBrowserMode(options) {
|
|
|
1075
1218
|
: "Model picker: skipped (strategy=ignore)");
|
|
1076
1219
|
}
|
|
1077
1220
|
const deepResearch = config.researchMode === "deep";
|
|
1078
|
-
|
|
1079
|
-
const thinkingTime = config.thinkingTime;
|
|
1080
|
-
if (thinkingTime && !deepResearch) {
|
|
1221
|
+
if (shouldApplyThinkingTimeSelection(config)) {
|
|
1081
1222
|
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
1082
|
-
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
1223
|
+
thinkingSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, config.thinkingTime, logger, thinkingTargetModel), {
|
|
1083
1224
|
retries: 2,
|
|
1084
1225
|
delayMs: 300,
|
|
1085
1226
|
onRetry: (attempt, error) => {
|
|
1086
1227
|
if (options.verbose) {
|
|
1087
|
-
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}`);
|
|
1088
1229
|
}
|
|
1089
1230
|
},
|
|
1090
1231
|
}));
|
|
@@ -1094,9 +1235,12 @@ export async function runBrowserMode(options) {
|
|
|
1094
1235
|
const acquireProfileLockIfNeeded = async () => {
|
|
1095
1236
|
if (profileLockTimeoutMs <= 0)
|
|
1096
1237
|
return;
|
|
1097
|
-
profileLock = await acquireProfileRunLock(userDataDir, {
|
|
1238
|
+
profileLock = await cancellation.acquire(() => acquireProfileRunLock(userDataDir, {
|
|
1098
1239
|
timeoutMs: profileLockTimeoutMs,
|
|
1099
1240
|
logger,
|
|
1241
|
+
signal: options.signal,
|
|
1242
|
+
}), async (lock) => {
|
|
1243
|
+
await lock?.release();
|
|
1100
1244
|
});
|
|
1101
1245
|
};
|
|
1102
1246
|
const releaseProfileLockIfHeld = async () => {
|
|
@@ -1104,7 +1248,7 @@ export async function runBrowserMode(options) {
|
|
|
1104
1248
|
return;
|
|
1105
1249
|
const handle = profileLock;
|
|
1106
1250
|
profileLock = null;
|
|
1107
|
-
await handle.release().catch(() => undefined);
|
|
1251
|
+
await withoutBrowserCancellation(() => handle.release()).catch(() => undefined);
|
|
1108
1252
|
};
|
|
1109
1253
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
1110
1254
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
@@ -1115,17 +1259,20 @@ export async function runBrowserMode(options) {
|
|
|
1115
1259
|
generatedBundle: a.generatedBundle === true,
|
|
1116
1260
|
}));
|
|
1117
1261
|
let inputOnlyAttachments = false;
|
|
1262
|
+
let attachmentNavigationUrl;
|
|
1118
1263
|
await raceWithDisconnect(clearPromptComposer(Runtime, logger));
|
|
1119
1264
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
1120
1265
|
if (submissionAttachments.length > 0) {
|
|
1121
1266
|
if (!DOM) {
|
|
1122
1267
|
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
1123
1268
|
}
|
|
1269
|
+
attachmentNavigationUrl = await raceWithDisconnect(captureComposerNavigationUrl(Runtime));
|
|
1124
1270
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
1125
1271
|
for (let attachmentIndex = 0; attachmentIndex < submissionAttachments.length; attachmentIndex += 1) {
|
|
1126
1272
|
const attachment = submissionAttachments[attachmentIndex];
|
|
1273
|
+
await raceWithDisconnect(assertComposerPlusStayedInPlace(Runtime, attachmentNavigationUrl));
|
|
1127
1274
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
1128
|
-
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 });
|
|
1129
1276
|
if (!uiConfirmed) {
|
|
1130
1277
|
inputOnlyAttachments = true;
|
|
1131
1278
|
}
|
|
@@ -1157,12 +1304,14 @@ export async function runBrowserMode(options) {
|
|
|
1157
1304
|
const providerState = {
|
|
1158
1305
|
runtime: Runtime,
|
|
1159
1306
|
input: Input,
|
|
1307
|
+
page: Page,
|
|
1160
1308
|
logger,
|
|
1161
1309
|
timeoutMs: config.timeoutMs,
|
|
1162
1310
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
1163
1311
|
attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
|
|
1164
1312
|
baselineTurns: baselineTurns ?? undefined,
|
|
1165
1313
|
attachmentNames: attachmentExpectations,
|
|
1314
|
+
attachmentNavigationUrl,
|
|
1166
1315
|
onPromptSubmitted: markPromptSubmitted,
|
|
1167
1316
|
};
|
|
1168
1317
|
const deepResearchTargetBaseline = deepResearch && client
|
|
@@ -1277,6 +1426,7 @@ export async function runBrowserMode(options) {
|
|
|
1277
1426
|
artifacts: savedArtifacts,
|
|
1278
1427
|
archive,
|
|
1279
1428
|
modelSelection: modelSelectionEvidence,
|
|
1429
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
1280
1430
|
tookMs: durationMs,
|
|
1281
1431
|
answerTokens: tokens,
|
|
1282
1432
|
answerChars: researchResult.text.length,
|
|
@@ -1301,6 +1451,7 @@ export async function runBrowserMode(options) {
|
|
|
1301
1451
|
const deadline = Date.now() + timeoutMs;
|
|
1302
1452
|
while (Date.now() < deadline) {
|
|
1303
1453
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1454
|
+
throwIfAssistantUiError(snapshot);
|
|
1304
1455
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1305
1456
|
if (text) {
|
|
1306
1457
|
const normalized = normalizeForComparison(text);
|
|
@@ -1528,6 +1679,7 @@ export async function runBrowserMode(options) {
|
|
|
1528
1679
|
let stableCount = 0;
|
|
1529
1680
|
while (Date.now() < deadline) {
|
|
1530
1681
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1682
|
+
throwIfAssistantUiError(snapshot);
|
|
1531
1683
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1532
1684
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
1533
1685
|
if (!isStillEcho) {
|
|
@@ -1557,6 +1709,7 @@ export async function runBrowserMode(options) {
|
|
|
1557
1709
|
let stableCycles = 0;
|
|
1558
1710
|
while (Date.now() < deadline) {
|
|
1559
1711
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1712
|
+
throwIfAssistantUiError(snapshot);
|
|
1560
1713
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1561
1714
|
if (text && text.length > bestText.length) {
|
|
1562
1715
|
bestText = text;
|
|
@@ -1711,6 +1864,7 @@ export async function runBrowserMode(options) {
|
|
|
1711
1864
|
savedFiles: fileArtifacts.savedFiles,
|
|
1712
1865
|
archive,
|
|
1713
1866
|
modelSelection: modelSelectionEvidence,
|
|
1867
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
1714
1868
|
tookMs: durationMs,
|
|
1715
1869
|
answerTokens,
|
|
1716
1870
|
answerChars,
|
|
@@ -1726,6 +1880,10 @@ export async function runBrowserMode(options) {
|
|
|
1726
1880
|
};
|
|
1727
1881
|
}
|
|
1728
1882
|
catch (error) {
|
|
1883
|
+
if (options.signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
1884
|
+
runStatus = "cancelled";
|
|
1885
|
+
throw new BrowserRunCancelledError();
|
|
1886
|
+
}
|
|
1729
1887
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
1730
1888
|
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
1731
1889
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
@@ -1812,150 +1970,154 @@ export async function runBrowserMode(options) {
|
|
|
1812
1970
|
}, normalizedError);
|
|
1813
1971
|
}
|
|
1814
1972
|
finally {
|
|
1815
|
-
await
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
catch {
|
|
1822
|
-
// ignore
|
|
1823
|
-
}
|
|
1824
|
-
// Close the isolated tab once the response has been fully captured to prevent
|
|
1825
|
-
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
1826
|
-
// so reattach can recover the response.
|
|
1827
|
-
const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
1828
|
-
runStatus,
|
|
1829
|
-
ownsTarget,
|
|
1830
|
-
keepBrowser: effectiveKeepBrowser,
|
|
1831
|
-
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
1832
|
-
});
|
|
1833
|
-
let keepBrowserOpen = shouldKeepLocalBrowserOpen({
|
|
1834
|
-
effectiveKeepBrowser,
|
|
1835
|
-
preserveBrowserOnError,
|
|
1836
|
-
usingCopiedProfile,
|
|
1837
|
-
});
|
|
1838
|
-
let cleanupProfileLock = null;
|
|
1839
|
-
let terminatedRecordedChrome = false;
|
|
1840
|
-
let otherActiveBrowserTabLeases = null;
|
|
1841
|
-
const hasOtherActiveLeases = async () => {
|
|
1842
|
-
if (!manualLogin || !tabLease) {
|
|
1843
|
-
return false;
|
|
1844
|
-
}
|
|
1845
|
-
if (otherActiveBrowserTabLeases === null) {
|
|
1846
|
-
otherActiveBrowserTabLeases = await hasOtherActiveBrowserTabLeases(userDataDir, tabLease.id);
|
|
1847
|
-
}
|
|
1848
|
-
return otherActiveBrowserTabLeases;
|
|
1849
|
-
};
|
|
1850
|
-
if (!keepBrowserOpen && manualLogin && tabLease) {
|
|
1851
|
-
const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
1852
|
-
if (cleanupLockTimeoutMs > 0) {
|
|
1853
|
-
cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
|
|
1854
|
-
timeoutMs: cleanupLockTimeoutMs,
|
|
1855
|
-
logger,
|
|
1856
|
-
sessionId: options.sessionId,
|
|
1857
|
-
}).catch(() => null);
|
|
1858
|
-
}
|
|
1859
|
-
keepBrowserOpen = await hasOtherActiveLeases().catch(() => false);
|
|
1860
|
-
if (keepBrowserOpen) {
|
|
1861
|
-
logger("[browser] Other ChatGPT tab leases still active; leaving shared Chrome running.");
|
|
1862
|
-
}
|
|
1863
|
-
else if (reusedChrome && !connectionClosedUnexpectedly) {
|
|
1864
|
-
terminatedRecordedChrome = await terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false);
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
const closeOwnedRunTarget = async () => {
|
|
1868
|
-
if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
const safeToClose = !effectiveKeepBrowser ||
|
|
1872
|
-
Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
|
|
1873
|
-
if (!safeToClose) {
|
|
1874
|
-
logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
|
|
1878
|
-
if (!closeConfirmed && effectiveKeepBrowser) {
|
|
1879
|
-
const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
|
|
1880
|
-
if (!replacementTargetId) {
|
|
1881
|
-
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();
|
|
1882
1979
|
}
|
|
1883
1980
|
}
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
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({
|
|
1887
1988
|
runStatus,
|
|
1888
1989
|
ownsTarget,
|
|
1889
|
-
connectionClosedUnexpectedly,
|
|
1890
|
-
manualLogin,
|
|
1891
1990
|
keepBrowser: effectiveKeepBrowser,
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
!chrome?.port) {
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
|
-
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1898
|
-
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1899
|
-
preserveOneBlank: true,
|
|
1991
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
1992
|
+
closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
|
|
1900
1993
|
});
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
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);
|
|
1909
2011
|
}
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
await closeOwnedRunTarget();
|
|
1915
|
-
await cleanupBlankTabs();
|
|
1916
|
-
}
|
|
1917
|
-
removeDialogHandler?.();
|
|
1918
|
-
removeTerminationHooks?.();
|
|
1919
|
-
if (!keepBrowserOpen) {
|
|
1920
|
-
if (!connectionClosedUnexpectedly) {
|
|
1921
|
-
try {
|
|
1922
|
-
if (!terminatedRecordedChrome) {
|
|
1923
|
-
await chrome.kill();
|
|
1924
|
-
}
|
|
2012
|
+
}
|
|
2013
|
+
const closeOwnedRunTarget = async () => {
|
|
2014
|
+
if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
|
|
2015
|
+
return;
|
|
1925
2016
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
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;
|
|
1928
2022
|
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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,
|
|
1932
2035
|
connectionClosedUnexpectedly,
|
|
1933
|
-
|
|
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,
|
|
1934
2065
|
});
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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`);
|
|
1938
2103
|
}
|
|
1939
2104
|
}
|
|
1940
2105
|
else {
|
|
1941
|
-
|
|
2106
|
+
detachKeptChromeProcess(chrome);
|
|
2107
|
+
if (!connectionClosedUnexpectedly) {
|
|
2108
|
+
logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
|
|
2109
|
+
}
|
|
1942
2110
|
}
|
|
1943
|
-
if (
|
|
1944
|
-
const
|
|
1945
|
-
|
|
2111
|
+
if (cleanupProfileLock) {
|
|
2112
|
+
const handle = cleanupProfileLock;
|
|
2113
|
+
cleanupProfileLock = null;
|
|
2114
|
+
await handle.release().catch(() => undefined);
|
|
1946
2115
|
}
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
if (!connectionClosedUnexpectedly) {
|
|
1951
|
-
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 });
|
|
1952
2119
|
}
|
|
1953
|
-
}
|
|
1954
|
-
if (cleanupProfileLock) {
|
|
1955
|
-
const handle = cleanupProfileLock;
|
|
1956
|
-
cleanupProfileLock = null;
|
|
1957
|
-
await handle.release().catch(() => undefined);
|
|
1958
|
-
}
|
|
2120
|
+
});
|
|
1959
2121
|
}
|
|
1960
2122
|
}
|
|
1961
2123
|
const DEFAULT_DEBUG_PORT = 9222;
|
|
@@ -2189,7 +2351,7 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
|
2189
2351
|
process: undefined,
|
|
2190
2352
|
};
|
|
2191
2353
|
}
|
|
2192
|
-
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
2354
|
+
async function runRemoteBrowserMode(promptText, attachments, config, logger, options, cancellation) {
|
|
2193
2355
|
const remoteChromeConfig = config.remoteChrome;
|
|
2194
2356
|
if (!remoteChromeConfig) {
|
|
2195
2357
|
throw new Error("Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.");
|
|
@@ -2202,6 +2364,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2202
2364
|
let lastUrl;
|
|
2203
2365
|
let promptSubmitted = false;
|
|
2204
2366
|
let modelSelectionEvidence;
|
|
2367
|
+
let thinkingSelectionEvidence;
|
|
2205
2368
|
let attachedExistingTab = false;
|
|
2206
2369
|
let ownsTarget = true;
|
|
2207
2370
|
let conversationUrlMonitor = null;
|
|
@@ -2258,22 +2421,24 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2258
2421
|
: resolveRemoteTabLeaseProfileDir(config);
|
|
2259
2422
|
if (remoteLeaseProfileDir) {
|
|
2260
2423
|
await mkdir(remoteLeaseProfileDir, { recursive: true });
|
|
2261
|
-
tabLease = await acquireBrowserTabLease(remoteLeaseProfileDir, {
|
|
2424
|
+
tabLease = await cancellation.acquire(() => acquireBrowserTabLease(remoteLeaseProfileDir, {
|
|
2262
2425
|
maxConcurrentTabs: config.maxConcurrentTabs,
|
|
2263
2426
|
timeoutMs: config.timeoutMs,
|
|
2264
2427
|
logger,
|
|
2265
2428
|
sessionId: options.sessionId,
|
|
2266
2429
|
chromeHost: host,
|
|
2267
2430
|
chromePort: port,
|
|
2268
|
-
|
|
2431
|
+
signal: options.signal,
|
|
2432
|
+
}), (lease) => lease.release());
|
|
2269
2433
|
}
|
|
2270
2434
|
if (config.browserTabRef) {
|
|
2271
|
-
const
|
|
2435
|
+
const tabRef = config.browserTabRef;
|
|
2436
|
+
const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
|
|
2272
2437
|
host,
|
|
2273
2438
|
port,
|
|
2274
|
-
ref:
|
|
2275
|
-
});
|
|
2276
|
-
client = attached.client;
|
|
2439
|
+
ref: tabRef,
|
|
2440
|
+
}), (attached) => attached.client.close());
|
|
2441
|
+
client = cancellation.client(attached.client);
|
|
2277
2442
|
remoteTargetId = attached.targetId ?? null;
|
|
2278
2443
|
lastUrl = attached.tab.url || lastUrl;
|
|
2279
2444
|
attachedExistingTab = true;
|
|
@@ -2281,10 +2446,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2281
2446
|
logger(`Attached to existing remote ChatGPT tab ${attached.targetId}${attached.tab.url ? ` (${attached.tab.url})` : ""}`);
|
|
2282
2447
|
}
|
|
2283
2448
|
else {
|
|
2284
|
-
connection = await connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
|
|
2285
|
-
approvalWaitMs:
|
|
2286
|
-
|
|
2287
|
-
|
|
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);
|
|
2288
2454
|
remoteTargetId = connection.targetId ?? null;
|
|
2289
2455
|
ownsTarget = true;
|
|
2290
2456
|
}
|
|
@@ -2393,16 +2559,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2393
2559
|
: "Model picker: skipped (strategy=ignore)");
|
|
2394
2560
|
}
|
|
2395
2561
|
const deepResearch = config.researchMode === "deep";
|
|
2396
|
-
|
|
2397
|
-
const thinkingTime = config.thinkingTime;
|
|
2398
|
-
if (thinkingTime && !deepResearch) {
|
|
2562
|
+
if (shouldApplyThinkingTimeSelection(config)) {
|
|
2399
2563
|
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
2400
|
-
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
2564
|
+
thinkingSelectionEvidence = await withRetries(() => ensureThinkingTime(Runtime, config.thinkingTime, logger, thinkingTargetModel), {
|
|
2401
2565
|
retries: 2,
|
|
2402
2566
|
delayMs: 300,
|
|
2403
2567
|
onRetry: (attempt, error) => {
|
|
2404
2568
|
if (options.verbose) {
|
|
2405
|
-
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}`);
|
|
2406
2570
|
}
|
|
2407
2571
|
},
|
|
2408
2572
|
});
|
|
@@ -2415,17 +2579,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2415
2579
|
name: path.basename(a.path),
|
|
2416
2580
|
generatedBundle: a.generatedBundle === true,
|
|
2417
2581
|
}));
|
|
2582
|
+
let attachmentNavigationUrl;
|
|
2418
2583
|
await clearPromptComposer(Runtime, logger);
|
|
2419
2584
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2420
2585
|
if (submissionAttachments.length > 0) {
|
|
2421
2586
|
if (!DOM) {
|
|
2422
2587
|
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
2423
2588
|
}
|
|
2589
|
+
attachmentNavigationUrl = await captureComposerNavigationUrl(Runtime);
|
|
2424
2590
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
2425
2591
|
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
2426
2592
|
for (const attachment of submissionAttachments) {
|
|
2593
|
+
await assertComposerPlusStayedInPlace(Runtime, attachmentNavigationUrl);
|
|
2427
2594
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
2428
|
-
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
2595
|
+
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM, navigationUrl: attachmentNavigationUrl }, attachment, logger);
|
|
2429
2596
|
await delay(500);
|
|
2430
2597
|
}
|
|
2431
2598
|
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
@@ -2453,12 +2620,14 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2453
2620
|
const providerState = {
|
|
2454
2621
|
runtime: Runtime,
|
|
2455
2622
|
input: Input,
|
|
2623
|
+
page: Page,
|
|
2456
2624
|
logger,
|
|
2457
2625
|
timeoutMs: config.timeoutMs,
|
|
2458
2626
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
2459
2627
|
attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
|
|
2460
2628
|
baselineTurns: baselineTurns ?? undefined,
|
|
2461
2629
|
attachmentNames: attachmentExpectations,
|
|
2630
|
+
attachmentNavigationUrl,
|
|
2462
2631
|
onPromptSubmitted: markPromptSubmitted,
|
|
2463
2632
|
};
|
|
2464
2633
|
const deepResearchTargetBaseline = deepResearch && client
|
|
@@ -2549,6 +2718,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2549
2718
|
artifacts: savedArtifacts,
|
|
2550
2719
|
archive,
|
|
2551
2720
|
modelSelection: modelSelectionEvidence,
|
|
2721
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
2552
2722
|
tookMs: durationMs,
|
|
2553
2723
|
answerTokens: tokens,
|
|
2554
2724
|
answerChars: researchResult.text.length,
|
|
@@ -2571,6 +2741,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2571
2741
|
const deadline = Date.now() + timeoutMs;
|
|
2572
2742
|
while (Date.now() < deadline) {
|
|
2573
2743
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
2744
|
+
throwIfAssistantUiError(snapshot);
|
|
2574
2745
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
2575
2746
|
if (text) {
|
|
2576
2747
|
const normalized = normalizeForComparison(text);
|
|
@@ -2792,6 +2963,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2792
2963
|
let stableCount = 0;
|
|
2793
2964
|
while (Date.now() < deadline) {
|
|
2794
2965
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
2966
|
+
throwIfAssistantUiError(snapshot);
|
|
2795
2967
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
2796
2968
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
2797
2969
|
if (!isStillEcho) {
|
|
@@ -2955,10 +3127,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2955
3127
|
savedFiles: fileArtifacts.savedFiles,
|
|
2956
3128
|
archive,
|
|
2957
3129
|
modelSelection: modelSelectionEvidence,
|
|
3130
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
2958
3131
|
controllerPid: process.pid,
|
|
2959
3132
|
};
|
|
2960
3133
|
}
|
|
2961
3134
|
catch (error) {
|
|
3135
|
+
if (options.signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
3136
|
+
runStatus = "cancelled";
|
|
3137
|
+
throw new BrowserRunCancelledError();
|
|
3138
|
+
}
|
|
2962
3139
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
2963
3140
|
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
2964
3141
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
@@ -2996,57 +3173,61 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2996
3173
|
});
|
|
2997
3174
|
}
|
|
2998
3175
|
finally {
|
|
2999
|
-
await
|
|
3000
|
-
|
|
3001
|
-
await
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
// ignore
|
|
3010
|
-
}
|
|
3011
|
-
removeDialogHandler?.();
|
|
3012
|
-
const keepRemoteBrowser = Boolean(config.keepBrowser);
|
|
3013
|
-
const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
3014
|
-
runStatus,
|
|
3015
|
-
ownsTarget,
|
|
3016
|
-
keepBrowser: keepRemoteBrowser,
|
|
3017
|
-
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
3018
|
-
});
|
|
3019
|
-
const closeOwnedRemoteTarget = async () => {
|
|
3020
|
-
if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
|
|
3021
|
-
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
|
+
});
|
|
3022
3186
|
}
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
if (!safeToClose) {
|
|
3026
|
-
logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
|
|
3027
|
-
return;
|
|
3187
|
+
catch {
|
|
3188
|
+
// ignore
|
|
3028
3189
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
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;
|
|
3034
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);
|
|
3035
3223
|
}
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
}
|
|
3044
|
-
else {
|
|
3045
|
-
await closeOwnedRemoteTarget();
|
|
3046
|
-
}
|
|
3047
|
-
// Don't kill remote Chrome - it's not ours to manage
|
|
3048
|
-
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
3049
|
-
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
|
+
});
|
|
3050
3231
|
}
|
|
3051
3232
|
}
|
|
3052
3233
|
export { estimateTokenCount } from "./utils.js";
|
|
@@ -3065,10 +3246,13 @@ export const __test__ = {
|
|
|
3065
3246
|
isImageOnlyUiChromeText,
|
|
3066
3247
|
listIgnoredRemoteChromeFlags,
|
|
3067
3248
|
normalizeAuthenticatedModelSelectionError,
|
|
3249
|
+
pollGeneratedImageOrTextAssistantResponse,
|
|
3068
3250
|
resolveManualLoginWaitMs,
|
|
3251
|
+
shouldApplyThinkingTimeSelection,
|
|
3069
3252
|
shouldCleanupBlankTabsAfterLastLease,
|
|
3070
3253
|
shouldCloseOwnedRunTargetAfterRun,
|
|
3071
3254
|
shouldKeepLocalBrowserOpen,
|
|
3255
|
+
releaseLocalBrowserTabLease,
|
|
3072
3256
|
waitForAssistantResponseWithReload,
|
|
3073
3257
|
};
|
|
3074
3258
|
export { syncCookies } from "./cookies.js";
|