@steipete/oracle 0.18.0 → 0.20.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 +76 -76
- 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 +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -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 +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- 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 +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- 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 +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -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/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- 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 +86 -24
- 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 +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -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
|
@@ -2,25 +2,30 @@ import { mkdtemp, rm, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import net from "node:net";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { claimBrowserTarget } from "./targetClaim.js";
|
|
5
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
8
|
import { copyChromeProfile } from "./profileCopy.js";
|
|
7
|
-
import {
|
|
9
|
+
import { BrowserCancellation, withoutBrowserCancellation } from "./cancellation.js";
|
|
10
|
+
import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, positionChromeWindowOnscreen, connectToRemoteChrome, connectWithNewTab, closeTab, createChromePageTarget, ensureChromePageTargetAfterClose, closeBlankChromeTabs, } from "./chromeLifecycle.js";
|
|
8
11
|
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";
|
|
12
|
+
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, captureComposerNavigationUrl, assertComposerPlusStayedInPlace, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
10
13
|
import { INPUT_SELECTORS } from "./constants.js";
|
|
11
14
|
import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
|
|
12
15
|
import { ensureThinkingTime } from "./actions/thinkingTime.js";
|
|
16
|
+
import { throwIfAssistantUiError } from "./actions/assistantResponse.js";
|
|
13
17
|
import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
|
|
14
18
|
import { activateDeepResearch, captureDeepResearchTargetKeys, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
|
|
15
19
|
import { estimateTokenCount, withRetries, delay } from "./utils.js";
|
|
16
20
|
import { formatElapsed } from "../oracle/format.js";
|
|
17
21
|
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY } from "./constants.js";
|
|
18
|
-
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
22
|
+
import { BrowserAutomationError, BrowserRunCancelledError } from "../oracle/errors.js";
|
|
23
|
+
import { buildAttachmentBasenameCollisionDetails, findAttachmentBasenameCollisions, formatAttachmentBasenameCollisionMessage, } from "./attachmentValidation.js";
|
|
19
24
|
import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
|
|
20
25
|
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
21
26
|
import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
22
27
|
import { connectionLostUserMessage, isRecoverableChromeDisconnect, probeChromeTargetLiveness, } from "./cdpLiveness.js";
|
|
23
|
-
import { acquireBrowserTabLease
|
|
28
|
+
import { acquireBrowserTabLease } from "./tabLeaseRegistry.js";
|
|
24
29
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
25
30
|
import { collectGeneratedImageArtifacts } from "./chatgptImages.js";
|
|
26
31
|
import { collectChatGptFileArtifacts } from "./chatgptFiles.js";
|
|
@@ -58,7 +63,7 @@ function isReattachableCaptureError(error) {
|
|
|
58
63
|
if (!(error instanceof BrowserAutomationError))
|
|
59
64
|
return false;
|
|
60
65
|
const stage = error.details?.stage;
|
|
61
|
-
return stage === "assistant-timeout" || stage === "assistant-recheck";
|
|
66
|
+
return (stage === "assistant-timeout" || stage === "assistant-recheck" || stage === "assistant-ui-error");
|
|
62
67
|
}
|
|
63
68
|
function classifyPreservedBrowserError(error, headless) {
|
|
64
69
|
if (headless)
|
|
@@ -86,6 +91,11 @@ export function shouldPreserveBrowserOnErrorForTest(error, headless) {
|
|
|
86
91
|
export function classifyPreservedBrowserErrorForTest(error, headless) {
|
|
87
92
|
return classifyPreservedBrowserError(error, headless);
|
|
88
93
|
}
|
|
94
|
+
function shouldApplyThinkingTimeSelection(config) {
|
|
95
|
+
// Deep Research uses the same effort picker, so research mode must not
|
|
96
|
+
// suppress an explicitly configured thinking-time selection.
|
|
97
|
+
return config.thinkingTime !== undefined;
|
|
98
|
+
}
|
|
89
99
|
const MAX_CHATGPT_UI_WARNING_CHARS = 300;
|
|
90
100
|
const MAX_CHATGPT_UI_WARNINGS = 3;
|
|
91
101
|
function classifyChatGptUiWarningText(text) {
|
|
@@ -318,6 +328,17 @@ function hasBrowserErrorCode(error, code) {
|
|
|
318
328
|
return (error instanceof BrowserAutomationError &&
|
|
319
329
|
error.details?.code === code);
|
|
320
330
|
}
|
|
331
|
+
function assertUniqueAttachmentBasenames(attachments, options) {
|
|
332
|
+
const collisions = findAttachmentBasenameCollisions(attachments);
|
|
333
|
+
if (collisions.length === 0)
|
|
334
|
+
return;
|
|
335
|
+
const collisionDetails = buildAttachmentBasenameCollisionDetails(collisions, (attachment) => attachment.displayPath || attachment.path);
|
|
336
|
+
throw new BrowserAutomationError(formatAttachmentBasenameCollisionMessage(options.subject, collisionDetails.collisions), {
|
|
337
|
+
stage: options.stage,
|
|
338
|
+
code: "attachment-basename-collision",
|
|
339
|
+
...collisionDetails,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
321
342
|
async function saveOptionalArtifact(operation, logger) {
|
|
322
343
|
try {
|
|
323
344
|
return await operation();
|
|
@@ -357,10 +378,12 @@ async function pollGeneratedImageOrTextAssistantResponse(Runtime, timeoutMs, min
|
|
|
357
378
|
const deadline = Date.now() + timeoutMs;
|
|
358
379
|
while (Date.now() < deadline) {
|
|
359
380
|
let snapshot = await readAssistantSnapshot(Runtime, minTurnIndex, expectedConversationId).catch(() => null);
|
|
381
|
+
throwIfAssistantUiError(snapshot);
|
|
360
382
|
if (!snapshot && typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex)) {
|
|
361
383
|
const relaxedSnapshot = await readAssistantSnapshot(Runtime, undefined, expectedConversationId).catch(() => null);
|
|
362
384
|
const relaxedHtml = typeof relaxedSnapshot?.html === "string" ? relaxedSnapshot.html : "";
|
|
363
|
-
if (
|
|
385
|
+
if (!relaxedSnapshot?.uiError &&
|
|
386
|
+
relaxedHtml.includes("/backend-api/estuary/content?id=file_")) {
|
|
364
387
|
snapshot = relaxedSnapshot;
|
|
365
388
|
}
|
|
366
389
|
}
|
|
@@ -491,6 +514,13 @@ async function runSubmissionWithRecovery({ prompt, attachments, fallbackSubmissi
|
|
|
491
514
|
if (fallbackSubmission && isPromptTooLarge && !usedFallbackSubmission) {
|
|
492
515
|
usedFallbackSubmission = true;
|
|
493
516
|
logger("[browser] Inline prompt too large; retrying with file uploads.");
|
|
517
|
+
if (fallbackSubmission.prepare) {
|
|
518
|
+
await fallbackSubmission.prepare();
|
|
519
|
+
}
|
|
520
|
+
assertUniqueAttachmentBasenames(fallbackSubmission.attachments, {
|
|
521
|
+
stage: "upload-fallback",
|
|
522
|
+
subject: "The inline prompt was too large, but its upload fallback",
|
|
523
|
+
});
|
|
494
524
|
await prepareFallbackSubmission();
|
|
495
525
|
currentPrompt = fallbackSubmission.prompt;
|
|
496
526
|
currentAttachments = fallbackSubmission.attachments;
|
|
@@ -526,24 +556,20 @@ export function isLocalChromeHostForTest(host) {
|
|
|
526
556
|
return isLocalChromeHost(host);
|
|
527
557
|
}
|
|
528
558
|
async function closeRemoteConnectionAfterRun(options) {
|
|
529
|
-
if (options.connectionClosedUnexpectedly) {
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
559
|
if (!options.connection) {
|
|
533
560
|
await options.client?.close();
|
|
534
561
|
return;
|
|
535
562
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
else {
|
|
540
|
-
await options.client?.close();
|
|
541
|
-
}
|
|
563
|
+
await options.connection.close({
|
|
564
|
+
preserveTarget: options.connectionClosedUnexpectedly || options.preserveTarget,
|
|
565
|
+
});
|
|
542
566
|
}
|
|
543
567
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
544
|
-
return (options.
|
|
545
|
-
options.
|
|
546
|
-
|
|
568
|
+
return (options.ownsTarget &&
|
|
569
|
+
((options.runStatus === "cancelled" &&
|
|
570
|
+
(options.closeOwnedTabOnCancel ?? !options.keepBrowser)) ||
|
|
571
|
+
(options.runStatus === "complete" &&
|
|
572
|
+
(Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser))));
|
|
547
573
|
}
|
|
548
574
|
function shouldCleanupBlankTabsAfterLastLease(options) {
|
|
549
575
|
return (options.runStatus === "complete" &&
|
|
@@ -553,6 +579,79 @@ function shouldCleanupBlankTabsAfterLastLease(options) {
|
|
|
553
579
|
options.keepBrowser &&
|
|
554
580
|
Boolean(options.chromePort));
|
|
555
581
|
}
|
|
582
|
+
async function releaseLocalBrowserTabLease(options) {
|
|
583
|
+
let decisionObserved = false;
|
|
584
|
+
let keepBrowserOpen = false;
|
|
585
|
+
let terminationHandled = false;
|
|
586
|
+
let otherLeasesRemain = false;
|
|
587
|
+
let releaseError;
|
|
588
|
+
try {
|
|
589
|
+
await options.lease.release({
|
|
590
|
+
onRelease: async ({ isLastLease }) => {
|
|
591
|
+
decisionObserved = true;
|
|
592
|
+
if (!isLastLease) {
|
|
593
|
+
// Record this before any best-effort tab cleanup so a cleanup failure can
|
|
594
|
+
// never fall through into terminating Chrome used by another lease.
|
|
595
|
+
keepBrowserOpen = true;
|
|
596
|
+
otherLeasesRemain = true;
|
|
597
|
+
}
|
|
598
|
+
await options.closeOwnedRunTarget().catch(() => undefined);
|
|
599
|
+
if (!isLastLease) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await options.cleanupBlankTabs().catch(() => undefined);
|
|
603
|
+
if (options.terminateSharedChrome) {
|
|
604
|
+
options.logger(`[browser] ChatGPT browser slot ${options.lease.id.slice(0, 8)} is final; ` +
|
|
605
|
+
`terminating shared Chrome (${formatBrowserLeaseDiagnostics(options)}).`);
|
|
606
|
+
const terminated = await options.terminateSharedChrome().catch(() => false);
|
|
607
|
+
if (terminated) {
|
|
608
|
+
terminationHandled = true;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// A reused Chrome handle may have a no-op kill implementation. Never
|
|
612
|
+
// claim cleanup or fall through into an unverified lock-free kill.
|
|
613
|
+
keepBrowserOpen = true;
|
|
614
|
+
options.logger("[browser] Could not verify shared Chrome termination; leaving it available for reuse.");
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
releaseError = error instanceof Error ? error : new Error(String(error));
|
|
622
|
+
if (!terminationHandled)
|
|
623
|
+
keepBrowserOpen = true;
|
|
624
|
+
options.logger(`[browser] Failed to release the ChatGPT browser slot registry lock; restart Oracle/Codex MCP before another browser run: ${releaseError.message}`);
|
|
625
|
+
}
|
|
626
|
+
if (!decisionObserved) {
|
|
627
|
+
options.logger("[browser] Could not verify final ChatGPT tab lease; leaving shared Chrome running.");
|
|
628
|
+
return {
|
|
629
|
+
keepBrowserOpen: true,
|
|
630
|
+
terminationHandled: false,
|
|
631
|
+
...(releaseError ? { releaseError } : {}),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (otherLeasesRemain) {
|
|
635
|
+
options.logger(`[browser] Other ChatGPT tab leases still active; leaving shared Chrome running; ` +
|
|
636
|
+
`browser slot ${options.lease.id.slice(0, 8)} is non-final ` +
|
|
637
|
+
`(${formatBrowserLeaseDiagnostics(options)}).`);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
keepBrowserOpen,
|
|
641
|
+
terminationHandled,
|
|
642
|
+
...(releaseError ? { releaseError } : {}),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
function formatBrowserLeaseDiagnostics(options) {
|
|
646
|
+
return [
|
|
647
|
+
`session=${options.sessionId ?? "unknown"}`,
|
|
648
|
+
`controllerPid=${process.pid}`,
|
|
649
|
+
`chromePid=${options.chromePid ?? "unknown"}`,
|
|
650
|
+
`chromePort=${options.chromePort ?? "unknown"}`,
|
|
651
|
+
`target=${options.chromeTargetId ?? "unknown"}`,
|
|
652
|
+
`launch=${options.launchDisposition ?? "unknown"}`,
|
|
653
|
+
].join("; ");
|
|
654
|
+
}
|
|
556
655
|
function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
|
|
557
656
|
return {
|
|
558
657
|
requestedModel: desiredModel ?? null,
|
|
@@ -565,11 +664,25 @@ function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
|
|
|
565
664
|
};
|
|
566
665
|
}
|
|
567
666
|
export async function runBrowserMode(options) {
|
|
667
|
+
const cancellation = new BrowserCancellation(options.signal, options.log);
|
|
668
|
+
try {
|
|
669
|
+
cancellation.check();
|
|
670
|
+
return await cancellation.run(() => runBrowserModeInternal(options, cancellation));
|
|
671
|
+
}
|
|
672
|
+
finally {
|
|
673
|
+
cancellation.dispose();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function runBrowserModeInternal(options, cancellation) {
|
|
677
|
+
const attachments = options.attachments ?? [];
|
|
678
|
+
assertUniqueAttachmentBasenames(attachments, {
|
|
679
|
+
stage: "upload",
|
|
680
|
+
subject: "Browser upload",
|
|
681
|
+
});
|
|
568
682
|
const promptText = options.prompt?.trim();
|
|
569
683
|
if (!promptText) {
|
|
570
684
|
throw new Error("Prompt text is required when using browser mode.");
|
|
571
685
|
}
|
|
572
|
-
const attachments = options.attachments ?? [];
|
|
573
686
|
const fallbackSubmission = options.fallbackSubmission;
|
|
574
687
|
let config = resolveBrowserConfig(options.config);
|
|
575
688
|
const usingCopiedProfile = Boolean(config.copyProfileSource);
|
|
@@ -595,7 +708,11 @@ export async function runBrowserMode(options) {
|
|
|
595
708
|
let lastTargetId;
|
|
596
709
|
let lastUrl;
|
|
597
710
|
let promptSubmitted = false;
|
|
711
|
+
let ownedRecoveryTarget;
|
|
712
|
+
const targetClaimId = randomUUID();
|
|
598
713
|
let modelSelectionEvidence;
|
|
714
|
+
let thinkingSelectionEvidence;
|
|
715
|
+
let researchPlan;
|
|
599
716
|
let tabLease = null;
|
|
600
717
|
let conversationUrlMonitor = null;
|
|
601
718
|
const emitRuntimeHint = async () => {
|
|
@@ -611,8 +728,10 @@ export async function runBrowserMode(options) {
|
|
|
611
728
|
tabUrl: lastUrl,
|
|
612
729
|
conversationId,
|
|
613
730
|
promptSubmitted,
|
|
731
|
+
ownedRecoveryTarget,
|
|
614
732
|
userDataDir,
|
|
615
733
|
controllerPid: process.pid,
|
|
734
|
+
researchPlan,
|
|
616
735
|
};
|
|
617
736
|
try {
|
|
618
737
|
await runtimeHintCb?.(hint, modelSelectionEvidence);
|
|
@@ -646,7 +765,7 @@ export async function runBrowserMode(options) {
|
|
|
646
765
|
logger(line);
|
|
647
766
|
}
|
|
648
767
|
if (config.attachRunning) {
|
|
649
|
-
const attached = await resolveAttachRunningConnection(config, logger);
|
|
768
|
+
const attached = await cancellation.call(() => resolveAttachRunningConnection(config, logger));
|
|
650
769
|
config = {
|
|
651
770
|
...config,
|
|
652
771
|
remoteChrome: { host: attached.host, port: attached.port },
|
|
@@ -656,7 +775,7 @@ export async function runBrowserMode(options) {
|
|
|
656
775
|
}
|
|
657
776
|
if (!config.remoteChrome && !config.manualLogin) {
|
|
658
777
|
const preferredPort = config.debugPort ?? DEFAULT_DEBUG_PORT;
|
|
659
|
-
const availablePort = await pickAvailableDebugPort(preferredPort, logger);
|
|
778
|
+
const availablePort = await cancellation.call(() => pickAvailableDebugPort(preferredPort, logger));
|
|
660
779
|
if (availablePort !== preferredPort) {
|
|
661
780
|
logger(`DevTools port ${preferredPort} busy; using ${availablePort} to avoid attaching to stray Chrome.`);
|
|
662
781
|
}
|
|
@@ -669,7 +788,7 @@ export async function runBrowserMode(options) {
|
|
|
669
788
|
if (ignoredFlags.length > 0) {
|
|
670
789
|
logger(`Note: --remote-chrome ignores local Chrome flags (${ignoredFlags.join(", ")}).`);
|
|
671
790
|
}
|
|
672
|
-
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
791
|
+
return runRemoteBrowserMode(promptText, attachments, config, logger, options, cancellation);
|
|
673
792
|
}
|
|
674
793
|
const manualLogin = Boolean(config.manualLogin);
|
|
675
794
|
if (manualLogin && usingCopiedProfile) {
|
|
@@ -683,70 +802,82 @@ export async function runBrowserMode(options) {
|
|
|
683
802
|
: defaultManualLoginProfileDir();
|
|
684
803
|
const userDataDir = manualLogin
|
|
685
804
|
? manualProfileDir
|
|
686
|
-
: await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
|
|
805
|
+
: await cancellation.acquire(async () => mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-")), (dir) => rm(dir, { recursive: true, force: true }));
|
|
687
806
|
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
userDataDir,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
807
|
+
try {
|
|
808
|
+
if (manualLogin) {
|
|
809
|
+
// Learned: manual login reuses a persistent profile so cookies/SSO survive.
|
|
810
|
+
await cancellation.call(() => mkdir(userDataDir, { recursive: true }));
|
|
811
|
+
logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
|
|
812
|
+
await cancellation.call(() => assertManualLoginProfileReadyForRun({ userDataDir, keepBrowser: effectiveKeepBrowser }));
|
|
813
|
+
}
|
|
814
|
+
else if (config.copyProfileSource) {
|
|
815
|
+
const copying = copyChromeProfile(config.copyProfileSource, userDataDir, config.chromeProfile);
|
|
816
|
+
const copiedProfileDirectory = await cancellation.race(copying.finally(async () => {
|
|
817
|
+
if (options.signal?.aborted)
|
|
818
|
+
await withoutBrowserCancellation(() => rm(userDataDir, { recursive: true, force: true }));
|
|
819
|
+
}));
|
|
820
|
+
config = { ...config, chromeProfile: copiedProfileDirectory };
|
|
821
|
+
logger(`Seeded temporary Chrome profile ${copiedProfileDirectory} from ${config.copyProfileSource} (copy-profile mode; signed-in session reused without manual login)`);
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
logger(`Created temporary Chrome profile at ${userDataDir}`);
|
|
825
|
+
}
|
|
701
826
|
}
|
|
702
|
-
|
|
703
|
-
|
|
827
|
+
catch (error) {
|
|
828
|
+
if (!manualLogin)
|
|
829
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
830
|
+
throw error;
|
|
704
831
|
}
|
|
705
832
|
if (manualLogin) {
|
|
706
|
-
tabLease = await acquireBrowserTabLease(userDataDir, {
|
|
833
|
+
tabLease = await cancellation.acquire(() => acquireBrowserTabLease(userDataDir, {
|
|
707
834
|
maxConcurrentTabs: config.maxConcurrentTabs,
|
|
708
835
|
timeoutMs: config.timeoutMs,
|
|
709
836
|
logger,
|
|
710
837
|
sessionId: options.sessionId,
|
|
711
|
-
|
|
838
|
+
signal: options.signal,
|
|
839
|
+
}), (lease) => lease.release());
|
|
712
840
|
}
|
|
713
841
|
let acquiredChrome;
|
|
714
842
|
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);
|
|
843
|
+
if (manualLogin) {
|
|
844
|
+
acquiredChrome = await cancellation.acquire(() => acquireManualLoginChromeForRun(userDataDir, config, logger, options.sessionId), async ({ chrome }) => {
|
|
845
|
+
detachKeptChromeProcess(chrome);
|
|
846
|
+
});
|
|
730
847
|
}
|
|
731
|
-
|
|
732
|
-
await
|
|
848
|
+
else {
|
|
849
|
+
const launched = await cancellation.acquire(() => launchChrome({ ...config, remoteChrome: config.remoteChrome }, userDataDir, logger), async (chrome) => {
|
|
850
|
+
try {
|
|
851
|
+
await chrome.kill();
|
|
852
|
+
}
|
|
853
|
+
finally {
|
|
854
|
+
await rm(userDataDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
acquiredChrome = { chrome: launched, reusedChrome: null };
|
|
733
858
|
}
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
await withoutBrowserCancellation(async () => {
|
|
862
|
+
if (tabLease) {
|
|
863
|
+
const handle = tabLease;
|
|
864
|
+
tabLease = null;
|
|
865
|
+
await handle.release().catch(() => undefined);
|
|
866
|
+
}
|
|
867
|
+
if (!manualLogin)
|
|
868
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
869
|
+
});
|
|
734
870
|
throw error;
|
|
735
871
|
}
|
|
736
872
|
const { chrome, reusedChrome } = acquiredChrome;
|
|
737
873
|
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
738
|
-
if (tabLease) {
|
|
739
|
-
await tabLease.update({
|
|
740
|
-
chromeHost,
|
|
741
|
-
chromePort: chrome.port,
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
874
|
let removeTerminationHooks = null;
|
|
745
875
|
try {
|
|
746
876
|
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
747
877
|
isInFlight: () => runStatus !== "complete",
|
|
748
878
|
emitRuntimeHint,
|
|
749
879
|
preserveUserDataDir: manualLogin,
|
|
880
|
+
preserveSharedChromeOnSignal: manualLogin,
|
|
750
881
|
// copy-profile is a throwaway copy of a signed-in profile; never leave it on disk.
|
|
751
882
|
forceProfileCleanup: usingCopiedProfile,
|
|
752
883
|
});
|
|
@@ -768,14 +899,17 @@ export async function runBrowserMode(options) {
|
|
|
768
899
|
let appliedCookies = 0;
|
|
769
900
|
let preserveBrowserOnError = false;
|
|
770
901
|
try {
|
|
902
|
+
if (tabLease)
|
|
903
|
+
await cancellation.call(() => tabLease.update({ chromeHost, chromePort: chrome.port }));
|
|
771
904
|
try {
|
|
772
905
|
if (config.browserTabRef) {
|
|
773
|
-
const
|
|
906
|
+
const tabRef = config.browserTabRef;
|
|
907
|
+
const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
|
|
774
908
|
host: chromeHost,
|
|
775
909
|
port: chrome.port,
|
|
776
|
-
ref:
|
|
777
|
-
});
|
|
778
|
-
client = attached.client;
|
|
910
|
+
ref: tabRef,
|
|
911
|
+
}), (attached) => attached.client.close());
|
|
912
|
+
client = cancellation.client(attached.client);
|
|
779
913
|
isolatedTargetId = attached.targetId ?? null;
|
|
780
914
|
lastTargetId = attached.targetId ?? undefined;
|
|
781
915
|
lastUrl = attached.tab.url || lastUrl;
|
|
@@ -785,14 +919,26 @@ export async function runBrowserMode(options) {
|
|
|
785
919
|
else {
|
|
786
920
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
787
921
|
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
788
|
-
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
922
|
+
const connection = await cancellation.acquire(() => connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
789
923
|
fallbackToDefault: !strictTabIsolation,
|
|
790
924
|
retries: devtoolsRetries,
|
|
791
925
|
retryDelayMs: 500,
|
|
926
|
+
}), async (connection) => {
|
|
927
|
+
await connection.client.close().catch(() => undefined);
|
|
928
|
+
if (connection.targetId)
|
|
929
|
+
await closeTab(chrome.port, connection.targetId, logger, chromeHost);
|
|
792
930
|
});
|
|
793
|
-
client = connection.client;
|
|
931
|
+
client = cancellation.client(connection.client);
|
|
794
932
|
isolatedTargetId = connection.targetId ?? null;
|
|
795
|
-
ownsTarget =
|
|
933
|
+
ownsTarget = Boolean(connection.targetId);
|
|
934
|
+
if (connection.targetId && (!config.keepBrowser || options.closeOwnedTabOnComplete)) {
|
|
935
|
+
ownedRecoveryTarget = {
|
|
936
|
+
host: chromeHost,
|
|
937
|
+
port: chrome.port,
|
|
938
|
+
targetId: connection.targetId,
|
|
939
|
+
claimId: targetClaimId,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
796
942
|
}
|
|
797
943
|
if (tabLease && isolatedTargetId) {
|
|
798
944
|
await tabLease.update({
|
|
@@ -840,21 +986,30 @@ export async function runBrowserMode(options) {
|
|
|
840
986
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
841
987
|
: undefined,
|
|
842
988
|
promptSubmitted,
|
|
989
|
+
ownedRecoveryTarget,
|
|
843
990
|
controllerPid: process.pid,
|
|
991
|
+
researchPlan,
|
|
844
992
|
},
|
|
845
993
|
}));
|
|
846
994
|
})();
|
|
847
995
|
});
|
|
848
996
|
});
|
|
849
|
-
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
997
|
+
const raceWithDisconnect = (promise) => cancellation.race(Promise.race([promise, disconnectPromise]));
|
|
850
998
|
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
851
999
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
852
1000
|
if (DOM && typeof DOM.enable === "function") {
|
|
853
1001
|
domainEnablers.push(DOM.enable());
|
|
854
1002
|
}
|
|
855
1003
|
await Promise.all(domainEnablers);
|
|
1004
|
+
if (config.browserTabRef)
|
|
1005
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
856
1006
|
if (!config.headless && config.hideWindow) {
|
|
857
|
-
await positionChromeWindowOffscreen(client, logger);
|
|
1007
|
+
await positionChromeWindowOffscreen(client, userDataDir, logger);
|
|
1008
|
+
}
|
|
1009
|
+
else if (!config.headless) {
|
|
1010
|
+
// Persistent profiles can retain bounds from a prior hidden run. Visible
|
|
1011
|
+
// local runs must actively restore the Oracle-owned Chrome window.
|
|
1012
|
+
await positionChromeWindowOnscreen(client, userDataDir, logger);
|
|
858
1013
|
}
|
|
859
1014
|
// The send button is clicked with trusted CDP input events at viewport
|
|
860
1015
|
// coordinates, which ChatGPT silently drops when the window is hidden or
|
|
@@ -1076,16 +1231,14 @@ export async function runBrowserMode(options) {
|
|
|
1076
1231
|
: "Model picker: skipped (strategy=ignore)");
|
|
1077
1232
|
}
|
|
1078
1233
|
const deepResearch = config.researchMode === "deep";
|
|
1079
|
-
|
|
1080
|
-
const thinkingTime = config.thinkingTime;
|
|
1081
|
-
if (thinkingTime && !deepResearch) {
|
|
1234
|
+
if (shouldApplyThinkingTimeSelection(config)) {
|
|
1082
1235
|
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
1083
|
-
await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
1236
|
+
thinkingSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, config.thinkingTime, logger, thinkingTargetModel), {
|
|
1084
1237
|
retries: 2,
|
|
1085
1238
|
delayMs: 300,
|
|
1086
1239
|
onRetry: (attempt, error) => {
|
|
1087
1240
|
if (options.verbose) {
|
|
1088
|
-
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
1241
|
+
logger(`[retry] Thinking time (${config.thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
1089
1242
|
}
|
|
1090
1243
|
},
|
|
1091
1244
|
}));
|
|
@@ -1095,9 +1248,12 @@ export async function runBrowserMode(options) {
|
|
|
1095
1248
|
const acquireProfileLockIfNeeded = async () => {
|
|
1096
1249
|
if (profileLockTimeoutMs <= 0)
|
|
1097
1250
|
return;
|
|
1098
|
-
profileLock = await acquireProfileRunLock(userDataDir, {
|
|
1251
|
+
profileLock = await cancellation.acquire(() => acquireProfileRunLock(userDataDir, {
|
|
1099
1252
|
timeoutMs: profileLockTimeoutMs,
|
|
1100
1253
|
logger,
|
|
1254
|
+
signal: options.signal,
|
|
1255
|
+
}), async (lock) => {
|
|
1256
|
+
await lock?.release();
|
|
1101
1257
|
});
|
|
1102
1258
|
};
|
|
1103
1259
|
const releaseProfileLockIfHeld = async () => {
|
|
@@ -1105,9 +1261,10 @@ export async function runBrowserMode(options) {
|
|
|
1105
1261
|
return;
|
|
1106
1262
|
const handle = profileLock;
|
|
1107
1263
|
profileLock = null;
|
|
1108
|
-
await handle.release().catch(() => undefined);
|
|
1264
|
+
await withoutBrowserCancellation(() => handle.release()).catch(() => undefined);
|
|
1109
1265
|
};
|
|
1110
1266
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
1267
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
1111
1268
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
1112
1269
|
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
1113
1270
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
@@ -1116,17 +1273,20 @@ export async function runBrowserMode(options) {
|
|
|
1116
1273
|
generatedBundle: a.generatedBundle === true,
|
|
1117
1274
|
}));
|
|
1118
1275
|
let inputOnlyAttachments = false;
|
|
1276
|
+
let attachmentNavigationUrl;
|
|
1119
1277
|
await raceWithDisconnect(clearPromptComposer(Runtime, logger));
|
|
1120
1278
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
1121
1279
|
if (submissionAttachments.length > 0) {
|
|
1122
1280
|
if (!DOM) {
|
|
1123
1281
|
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
1124
1282
|
}
|
|
1283
|
+
attachmentNavigationUrl = await raceWithDisconnect(captureComposerNavigationUrl(Runtime));
|
|
1125
1284
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
1126
1285
|
for (let attachmentIndex = 0; attachmentIndex < submissionAttachments.length; attachmentIndex += 1) {
|
|
1127
1286
|
const attachment = submissionAttachments[attachmentIndex];
|
|
1287
|
+
await raceWithDisconnect(assertComposerPlusStayedInPlace(Runtime, attachmentNavigationUrl));
|
|
1128
1288
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
1129
|
-
const uiConfirmed = await uploadAttachmentFile({ runtime: Runtime, dom: DOM, input: Input }, attachment, logger, { expectedCount: attachmentIndex + 1 });
|
|
1289
|
+
const uiConfirmed = await uploadAttachmentFile({ runtime: Runtime, dom: DOM, input: Input }, attachment, logger, { expectedCount: attachmentIndex + 1, navigationUrl: attachmentNavigationUrl });
|
|
1130
1290
|
if (!uiConfirmed) {
|
|
1131
1291
|
inputOnlyAttachments = true;
|
|
1132
1292
|
}
|
|
@@ -1158,13 +1318,16 @@ export async function runBrowserMode(options) {
|
|
|
1158
1318
|
const providerState = {
|
|
1159
1319
|
runtime: Runtime,
|
|
1160
1320
|
input: Input,
|
|
1321
|
+
page: Page,
|
|
1161
1322
|
logger,
|
|
1162
1323
|
timeoutMs: config.timeoutMs,
|
|
1163
1324
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
1164
1325
|
attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
|
|
1165
1326
|
baselineTurns: baselineTurns ?? undefined,
|
|
1166
1327
|
attachmentNames: attachmentExpectations,
|
|
1328
|
+
attachmentNavigationUrl,
|
|
1167
1329
|
onPromptSubmitted: markPromptSubmitted,
|
|
1330
|
+
webSearch: config.researchMode === "search",
|
|
1168
1331
|
};
|
|
1169
1332
|
const deepResearchTargetBaseline = deepResearch && client
|
|
1170
1333
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
@@ -1239,7 +1402,17 @@ export async function runBrowserMode(options) {
|
|
|
1239
1402
|
}
|
|
1240
1403
|
const imageArtifactMinTurnIndex = baselineTurns;
|
|
1241
1404
|
if (deepResearch) {
|
|
1242
|
-
await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger
|
|
1405
|
+
await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger, undefined, {
|
|
1406
|
+
Page,
|
|
1407
|
+
client,
|
|
1408
|
+
ignoredTargetKeys: deepResearchTargetKeys,
|
|
1409
|
+
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
1410
|
+
minTurnIndex: baselineTurns,
|
|
1411
|
+
onPlan: async (plan) => {
|
|
1412
|
+
researchPlan = plan;
|
|
1413
|
+
await emitRuntimeHint();
|
|
1414
|
+
},
|
|
1415
|
+
}));
|
|
1243
1416
|
const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
|
|
1244
1417
|
ignoredTargetKeys: deepResearchTargetKeys,
|
|
1245
1418
|
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
@@ -1278,6 +1451,7 @@ export async function runBrowserMode(options) {
|
|
|
1278
1451
|
artifacts: savedArtifacts,
|
|
1279
1452
|
archive,
|
|
1280
1453
|
modelSelection: modelSelectionEvidence,
|
|
1454
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
1281
1455
|
tookMs: durationMs,
|
|
1282
1456
|
answerTokens: tokens,
|
|
1283
1457
|
answerChars: researchResult.text.length,
|
|
@@ -1289,7 +1463,9 @@ export async function runBrowserMode(options) {
|
|
|
1289
1463
|
tabUrl: lastUrl,
|
|
1290
1464
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1291
1465
|
promptSubmitted,
|
|
1466
|
+
ownedRecoveryTarget,
|
|
1292
1467
|
controllerPid: process.pid,
|
|
1468
|
+
researchPlan,
|
|
1293
1469
|
};
|
|
1294
1470
|
}
|
|
1295
1471
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
@@ -1302,6 +1478,7 @@ export async function runBrowserMode(options) {
|
|
|
1302
1478
|
const deadline = Date.now() + timeoutMs;
|
|
1303
1479
|
while (Date.now() < deadline) {
|
|
1304
1480
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1481
|
+
throwIfAssistantUiError(snapshot);
|
|
1305
1482
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1306
1483
|
if (text) {
|
|
1307
1484
|
const normalized = normalizeForComparison(text);
|
|
@@ -1378,6 +1555,7 @@ export async function runBrowserMode(options) {
|
|
|
1378
1555
|
tabUrl: lastUrl,
|
|
1379
1556
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1380
1557
|
promptSubmitted,
|
|
1558
|
+
ownedRecoveryTarget,
|
|
1381
1559
|
controllerPid: process.pid,
|
|
1382
1560
|
},
|
|
1383
1561
|
});
|
|
@@ -1434,6 +1612,7 @@ export async function runBrowserMode(options) {
|
|
|
1434
1612
|
tabUrl: lastUrl,
|
|
1435
1613
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1436
1614
|
promptSubmitted,
|
|
1615
|
+
ownedRecoveryTarget,
|
|
1437
1616
|
controllerPid: process.pid,
|
|
1438
1617
|
};
|
|
1439
1618
|
throw await createAssistantTimeoutError({
|
|
@@ -1529,6 +1708,7 @@ export async function runBrowserMode(options) {
|
|
|
1529
1708
|
let stableCount = 0;
|
|
1530
1709
|
while (Date.now() < deadline) {
|
|
1531
1710
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1711
|
+
throwIfAssistantUiError(snapshot);
|
|
1532
1712
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1533
1713
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
1534
1714
|
if (!isStillEcho) {
|
|
@@ -1558,6 +1738,7 @@ export async function runBrowserMode(options) {
|
|
|
1558
1738
|
let stableCycles = 0;
|
|
1559
1739
|
while (Date.now() < deadline) {
|
|
1560
1740
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1741
|
+
throwIfAssistantUiError(snapshot);
|
|
1561
1742
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1562
1743
|
if (text && text.length > bestText.length) {
|
|
1563
1744
|
bestText = text;
|
|
@@ -1657,6 +1838,7 @@ export async function runBrowserMode(options) {
|
|
|
1657
1838
|
tabUrl: lastUrl,
|
|
1658
1839
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1659
1840
|
promptSubmitted,
|
|
1841
|
+
ownedRecoveryTarget,
|
|
1660
1842
|
controllerPid: process.pid,
|
|
1661
1843
|
},
|
|
1662
1844
|
}),
|
|
@@ -1712,6 +1894,7 @@ export async function runBrowserMode(options) {
|
|
|
1712
1894
|
savedFiles: fileArtifacts.savedFiles,
|
|
1713
1895
|
archive,
|
|
1714
1896
|
modelSelection: modelSelectionEvidence,
|
|
1897
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
1715
1898
|
tookMs: durationMs,
|
|
1716
1899
|
answerTokens,
|
|
1717
1900
|
answerChars,
|
|
@@ -1723,10 +1906,15 @@ export async function runBrowserMode(options) {
|
|
|
1723
1906
|
tabUrl: lastUrl,
|
|
1724
1907
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1725
1908
|
promptSubmitted,
|
|
1909
|
+
ownedRecoveryTarget,
|
|
1726
1910
|
controllerPid: process.pid,
|
|
1727
1911
|
};
|
|
1728
1912
|
}
|
|
1729
1913
|
catch (error) {
|
|
1914
|
+
if (options.signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
1915
|
+
runStatus = "cancelled";
|
|
1916
|
+
throw new BrowserRunCancelledError();
|
|
1917
|
+
}
|
|
1730
1918
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
1731
1919
|
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
1732
1920
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
@@ -1745,6 +1933,7 @@ export async function runBrowserMode(options) {
|
|
|
1745
1933
|
chromeTargetId: lastTargetId,
|
|
1746
1934
|
tabUrl: lastUrl,
|
|
1747
1935
|
promptSubmitted,
|
|
1936
|
+
ownedRecoveryTarget,
|
|
1748
1937
|
controllerPid: process.pid,
|
|
1749
1938
|
};
|
|
1750
1939
|
const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
|
|
@@ -1808,155 +1997,161 @@ export async function runBrowserMode(options) {
|
|
|
1808
1997
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1809
1998
|
: undefined,
|
|
1810
1999
|
promptSubmitted,
|
|
2000
|
+
ownedRecoveryTarget,
|
|
1811
2001
|
controllerPid: process.pid,
|
|
2002
|
+
researchPlan,
|
|
1812
2003
|
},
|
|
1813
2004
|
}, normalizedError);
|
|
1814
2005
|
}
|
|
1815
2006
|
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}.`);
|
|
2007
|
+
await withoutBrowserCancellation(async () => {
|
|
2008
|
+
stopThinkingMonitor?.();
|
|
2009
|
+
await conversationUrlMonitor?.stop();
|
|
2010
|
+
try {
|
|
2011
|
+
if (!connectionClosedUnexpectedly) {
|
|
2012
|
+
await client?.close();
|
|
1883
2013
|
}
|
|
1884
2014
|
}
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
2015
|
+
catch {
|
|
2016
|
+
// ignore
|
|
2017
|
+
}
|
|
2018
|
+
// Close the isolated tab once the response has been fully captured to prevent
|
|
2019
|
+
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
2020
|
+
// so reattach can recover the response.
|
|
2021
|
+
const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
1888
2022
|
runStatus,
|
|
1889
2023
|
ownsTarget,
|
|
1890
|
-
connectionClosedUnexpectedly,
|
|
1891
|
-
manualLogin,
|
|
1892
2024
|
keepBrowser: effectiveKeepBrowser,
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
!chrome?.port) {
|
|
1896
|
-
return;
|
|
1897
|
-
}
|
|
1898
|
-
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1899
|
-
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1900
|
-
preserveOneBlank: true,
|
|
2025
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
2026
|
+
closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
|
|
1901
2027
|
});
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2028
|
+
let keepBrowserOpen = (manualLogin && runStatus === "cancelled") ||
|
|
2029
|
+
shouldKeepLocalBrowserOpen({
|
|
2030
|
+
effectiveKeepBrowser,
|
|
2031
|
+
preserveBrowserOnError,
|
|
2032
|
+
usingCopiedProfile,
|
|
2033
|
+
});
|
|
2034
|
+
let cleanupProfileLock = null;
|
|
2035
|
+
let browserTerminationHandledByLease = false;
|
|
2036
|
+
let tabLeaseReleaseError;
|
|
2037
|
+
if (!keepBrowserOpen && manualLogin && tabLease) {
|
|
2038
|
+
const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
2039
|
+
if (cleanupLockTimeoutMs > 0) {
|
|
2040
|
+
cleanupProfileLock = await acquireProfileRunLock(userDataDir, {
|
|
2041
|
+
timeoutMs: cleanupLockTimeoutMs,
|
|
2042
|
+
logger,
|
|
2043
|
+
sessionId: options.sessionId,
|
|
2044
|
+
}).catch(() => null);
|
|
1910
2045
|
}
|
|
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
|
-
}
|
|
2046
|
+
}
|
|
2047
|
+
const closeOwnedRunTarget = async () => {
|
|
2048
|
+
if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
|
|
2049
|
+
return;
|
|
1926
2050
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
2051
|
+
const safeToClose = !keepBrowserOpen ||
|
|
2052
|
+
Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
|
|
2053
|
+
if (!safeToClose) {
|
|
2054
|
+
logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
|
|
2055
|
+
return;
|
|
1929
2056
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
2057
|
+
const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
|
|
2058
|
+
if (!closeConfirmed && keepBrowserOpen) {
|
|
2059
|
+
const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
|
|
2060
|
+
if (!replacementTargetId) {
|
|
2061
|
+
logger(`[browser] Chrome page retention could not be verified after closing ${isolatedTargetId}.`);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
const cleanupBlankTabs = async () => {
|
|
2066
|
+
if (!shouldCleanupBlankTabsAfterLastLease({
|
|
2067
|
+
runStatus,
|
|
2068
|
+
ownsTarget,
|
|
1933
2069
|
connectionClosedUnexpectedly,
|
|
1934
|
-
|
|
2070
|
+
manualLogin,
|
|
2071
|
+
keepBrowser: effectiveKeepBrowser,
|
|
2072
|
+
chromePort: chrome?.port,
|
|
2073
|
+
}) ||
|
|
2074
|
+
!chrome?.port) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
2078
|
+
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
2079
|
+
preserveOneBlank: true,
|
|
2080
|
+
});
|
|
2081
|
+
};
|
|
2082
|
+
if (tabLease) {
|
|
2083
|
+
const handle = tabLease;
|
|
2084
|
+
tabLease = null;
|
|
2085
|
+
const terminateSharedChrome = !keepBrowserOpen && manualLogin && !connectionClosedUnexpectedly
|
|
2086
|
+
? async () => terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false)
|
|
2087
|
+
: undefined;
|
|
2088
|
+
const releaseResult = await releaseLocalBrowserTabLease({
|
|
2089
|
+
lease: handle,
|
|
2090
|
+
closeOwnedRunTarget,
|
|
2091
|
+
cleanupBlankTabs,
|
|
2092
|
+
terminateSharedChrome,
|
|
2093
|
+
sessionId: options.sessionId,
|
|
2094
|
+
chromePid: chrome.pid,
|
|
2095
|
+
chromePort: chrome.port,
|
|
2096
|
+
chromeTargetId: isolatedTargetId,
|
|
2097
|
+
launchDisposition: reusedChrome ? "reused" : "launched",
|
|
2098
|
+
logger,
|
|
1935
2099
|
});
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2100
|
+
keepBrowserOpen ||= releaseResult.keepBrowserOpen;
|
|
2101
|
+
browserTerminationHandledByLease = releaseResult.terminationHandled;
|
|
2102
|
+
tabLeaseReleaseError = releaseResult.releaseError;
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
await closeOwnedRunTarget();
|
|
2106
|
+
await cleanupBlankTabs();
|
|
2107
|
+
}
|
|
2108
|
+
removeDialogHandler?.();
|
|
2109
|
+
removeTerminationHooks?.();
|
|
2110
|
+
if (!keepBrowserOpen) {
|
|
2111
|
+
if (!connectionClosedUnexpectedly) {
|
|
2112
|
+
try {
|
|
2113
|
+
if (!browserTerminationHandledByLease) {
|
|
2114
|
+
await chrome.kill();
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
catch {
|
|
2118
|
+
// ignore kill failures
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (manualLogin) {
|
|
2122
|
+
const shouldCleanup = await shouldCleanupManualLoginProfileState(userDataDir, logger.verbose ? logger : undefined, {
|
|
2123
|
+
connectionClosedUnexpectedly,
|
|
2124
|
+
host: chromeHost,
|
|
2125
|
+
});
|
|
2126
|
+
if (shouldCleanup) {
|
|
2127
|
+
// Preserve the persistent manual-login profile, but clear stale reattach hints.
|
|
2128
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
else {
|
|
2132
|
+
await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
|
|
2133
|
+
}
|
|
2134
|
+
if (!connectionClosedUnexpectedly) {
|
|
2135
|
+
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
2136
|
+
logger(`Cleanup ${runStatus} • ${totalSeconds.toFixed(1)}s total`);
|
|
1939
2137
|
}
|
|
1940
2138
|
}
|
|
1941
2139
|
else {
|
|
1942
|
-
|
|
2140
|
+
detachKeptChromeProcess(chrome);
|
|
2141
|
+
if (!connectionClosedUnexpectedly) {
|
|
2142
|
+
logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
|
|
2143
|
+
}
|
|
1943
2144
|
}
|
|
1944
|
-
if (
|
|
1945
|
-
const
|
|
1946
|
-
|
|
2145
|
+
if (cleanupProfileLock) {
|
|
2146
|
+
const handle = cleanupProfileLock;
|
|
2147
|
+
cleanupProfileLock = null;
|
|
2148
|
+
await handle.release().catch(() => undefined);
|
|
1947
2149
|
}
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (!connectionClosedUnexpectedly) {
|
|
1952
|
-
logger(`Chrome left running on port ${chrome.port} with profile ${userDataDir}`);
|
|
2150
|
+
if (tabLeaseReleaseError) {
|
|
2151
|
+
// 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.
|
|
2152
|
+
throw new Error("Failed to release the ChatGPT browser slot registry lock; restart Oracle/Codex MCP before another browser run.", { cause: tabLeaseReleaseError });
|
|
1953
2153
|
}
|
|
1954
|
-
}
|
|
1955
|
-
if (cleanupProfileLock) {
|
|
1956
|
-
const handle = cleanupProfileLock;
|
|
1957
|
-
cleanupProfileLock = null;
|
|
1958
|
-
await handle.release().catch(() => undefined);
|
|
1959
|
-
}
|
|
2154
|
+
});
|
|
1960
2155
|
}
|
|
1961
2156
|
}
|
|
1962
2157
|
const DEFAULT_DEBUG_PORT = 9222;
|
|
@@ -2190,7 +2385,7 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
|
2190
2385
|
process: undefined,
|
|
2191
2386
|
};
|
|
2192
2387
|
}
|
|
2193
|
-
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
2388
|
+
async function runRemoteBrowserMode(promptText, attachments, config, logger, options, cancellation) {
|
|
2194
2389
|
const remoteChromeConfig = config.remoteChrome;
|
|
2195
2390
|
if (!remoteChromeConfig) {
|
|
2196
2391
|
throw new Error("Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.");
|
|
@@ -2202,7 +2397,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2202
2397
|
let tabLease = null;
|
|
2203
2398
|
let lastUrl;
|
|
2204
2399
|
let promptSubmitted = false;
|
|
2400
|
+
let ownedRecoveryTarget;
|
|
2401
|
+
const targetClaimId = randomUUID();
|
|
2205
2402
|
let modelSelectionEvidence;
|
|
2403
|
+
let thinkingSelectionEvidence;
|
|
2404
|
+
let researchPlan;
|
|
2206
2405
|
let attachedExistingTab = false;
|
|
2207
2406
|
let ownsTarget = true;
|
|
2208
2407
|
let conversationUrlMonitor = null;
|
|
@@ -2220,7 +2419,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2220
2419
|
tabUrl: lastUrl,
|
|
2221
2420
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2222
2421
|
promptSubmitted,
|
|
2422
|
+
ownedRecoveryTarget,
|
|
2223
2423
|
controllerPid: process.pid,
|
|
2424
|
+
researchPlan,
|
|
2224
2425
|
}, modelSelectionEvidence);
|
|
2225
2426
|
await tabLease?.update({
|
|
2226
2427
|
chromeHost: host,
|
|
@@ -2259,22 +2460,24 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2259
2460
|
: resolveRemoteTabLeaseProfileDir(config);
|
|
2260
2461
|
if (remoteLeaseProfileDir) {
|
|
2261
2462
|
await mkdir(remoteLeaseProfileDir, { recursive: true });
|
|
2262
|
-
tabLease = await acquireBrowserTabLease(remoteLeaseProfileDir, {
|
|
2463
|
+
tabLease = await cancellation.acquire(() => acquireBrowserTabLease(remoteLeaseProfileDir, {
|
|
2263
2464
|
maxConcurrentTabs: config.maxConcurrentTabs,
|
|
2264
2465
|
timeoutMs: config.timeoutMs,
|
|
2265
2466
|
logger,
|
|
2266
2467
|
sessionId: options.sessionId,
|
|
2267
2468
|
chromeHost: host,
|
|
2268
2469
|
chromePort: port,
|
|
2269
|
-
|
|
2470
|
+
signal: options.signal,
|
|
2471
|
+
}), (lease) => lease.release());
|
|
2270
2472
|
}
|
|
2271
2473
|
if (config.browserTabRef) {
|
|
2272
|
-
const
|
|
2474
|
+
const tabRef = config.browserTabRef;
|
|
2475
|
+
const attached = await cancellation.acquire(() => connectToExistingChatGptTab({
|
|
2273
2476
|
host,
|
|
2274
2477
|
port,
|
|
2275
|
-
ref:
|
|
2276
|
-
});
|
|
2277
|
-
client = attached.client;
|
|
2478
|
+
ref: tabRef,
|
|
2479
|
+
}), (attached) => attached.client.close());
|
|
2480
|
+
client = cancellation.client(attached.client);
|
|
2278
2481
|
remoteTargetId = attached.targetId ?? null;
|
|
2279
2482
|
lastUrl = attached.tab.url || lastUrl;
|
|
2280
2483
|
attachedExistingTab = true;
|
|
@@ -2282,12 +2485,22 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2282
2485
|
logger(`Attached to existing remote ChatGPT tab ${attached.targetId}${attached.tab.url ? ` (${attached.tab.url})` : ""}`);
|
|
2283
2486
|
}
|
|
2284
2487
|
else {
|
|
2285
|
-
connection = await connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
|
|
2286
|
-
approvalWaitMs:
|
|
2287
|
-
|
|
2288
|
-
|
|
2488
|
+
connection = await cancellation.acquire(() => connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
|
|
2489
|
+
approvalWaitMs: browserWSEndpoint ? config.approvalWaitMs : undefined,
|
|
2490
|
+
fallbackToDefault: false,
|
|
2491
|
+
}), (connection) => connection.close());
|
|
2492
|
+
client = cancellation.client(connection.client);
|
|
2289
2493
|
remoteTargetId = connection.targetId ?? null;
|
|
2290
|
-
ownsTarget =
|
|
2494
|
+
ownsTarget = Boolean(connection.targetId);
|
|
2495
|
+
if (connection.targetId && (!config.keepBrowser || options.closeOwnedTabOnComplete)) {
|
|
2496
|
+
ownedRecoveryTarget = {
|
|
2497
|
+
host,
|
|
2498
|
+
port,
|
|
2499
|
+
targetId: connection.targetId,
|
|
2500
|
+
browserWSEndpoint,
|
|
2501
|
+
claimId: targetClaimId,
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2291
2504
|
}
|
|
2292
2505
|
if (tabLease && remoteTargetId) {
|
|
2293
2506
|
await tabLease.update({
|
|
@@ -2307,6 +2520,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2307
2520
|
domainEnablers.push(DOM.enable());
|
|
2308
2521
|
}
|
|
2309
2522
|
await Promise.all(domainEnablers);
|
|
2523
|
+
if (config.browserTabRef)
|
|
2524
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
2310
2525
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
2311
2526
|
await enableFocusEmulation(client, logger, "remote target");
|
|
2312
2527
|
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
@@ -2394,21 +2609,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2394
2609
|
: "Model picker: skipped (strategy=ignore)");
|
|
2395
2610
|
}
|
|
2396
2611
|
const deepResearch = config.researchMode === "deep";
|
|
2397
|
-
|
|
2398
|
-
const thinkingTime = config.thinkingTime;
|
|
2399
|
-
if (thinkingTime && !deepResearch) {
|
|
2612
|
+
if (shouldApplyThinkingTimeSelection(config)) {
|
|
2400
2613
|
const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
|
|
2401
|
-
await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
|
|
2614
|
+
thinkingSelectionEvidence = await withRetries(() => ensureThinkingTime(Runtime, config.thinkingTime, logger, thinkingTargetModel), {
|
|
2402
2615
|
retries: 2,
|
|
2403
2616
|
delayMs: 300,
|
|
2404
2617
|
onRetry: (attempt, error) => {
|
|
2405
2618
|
if (options.verbose) {
|
|
2406
|
-
logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
2619
|
+
logger(`[retry] Thinking time (${config.thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
|
|
2407
2620
|
}
|
|
2408
2621
|
},
|
|
2409
2622
|
});
|
|
2410
2623
|
}
|
|
2411
2624
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
2625
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
2412
2626
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
2413
2627
|
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
2414
2628
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
@@ -2416,17 +2630,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2416
2630
|
name: path.basename(a.path),
|
|
2417
2631
|
generatedBundle: a.generatedBundle === true,
|
|
2418
2632
|
}));
|
|
2633
|
+
let attachmentNavigationUrl;
|
|
2419
2634
|
await clearPromptComposer(Runtime, logger);
|
|
2420
2635
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2421
2636
|
if (submissionAttachments.length > 0) {
|
|
2422
2637
|
if (!DOM) {
|
|
2423
2638
|
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
2424
2639
|
}
|
|
2640
|
+
attachmentNavigationUrl = await captureComposerNavigationUrl(Runtime);
|
|
2425
2641
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
2426
2642
|
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
2427
2643
|
for (const attachment of submissionAttachments) {
|
|
2644
|
+
await assertComposerPlusStayedInPlace(Runtime, attachmentNavigationUrl);
|
|
2428
2645
|
logger(`Uploading attachment: ${attachment.displayPath}`);
|
|
2429
|
-
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
|
|
2646
|
+
await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM, navigationUrl: attachmentNavigationUrl }, attachment, logger);
|
|
2430
2647
|
await delay(500);
|
|
2431
2648
|
}
|
|
2432
2649
|
// Scale timeout based on number of files: base 30s + 15s per additional file
|
|
@@ -2454,13 +2671,16 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2454
2671
|
const providerState = {
|
|
2455
2672
|
runtime: Runtime,
|
|
2456
2673
|
input: Input,
|
|
2674
|
+
page: Page,
|
|
2457
2675
|
logger,
|
|
2458
2676
|
timeoutMs: config.timeoutMs,
|
|
2459
2677
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
2460
2678
|
attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
|
|
2461
2679
|
baselineTurns: baselineTurns ?? undefined,
|
|
2462
2680
|
attachmentNames: attachmentExpectations,
|
|
2681
|
+
attachmentNavigationUrl,
|
|
2463
2682
|
onPromptSubmitted: markPromptSubmitted,
|
|
2683
|
+
webSearch: config.researchMode === "search",
|
|
2464
2684
|
};
|
|
2465
2685
|
const deepResearchTargetBaseline = deepResearch && client
|
|
2466
2686
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
@@ -2511,7 +2731,17 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2511
2731
|
deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
|
|
2512
2732
|
const imageArtifactMinTurnIndex = baselineTurns;
|
|
2513
2733
|
if (deepResearch) {
|
|
2514
|
-
await waitForResearchPlanAutoConfirm(Runtime, logger
|
|
2734
|
+
await waitForResearchPlanAutoConfirm(Runtime, logger, undefined, {
|
|
2735
|
+
Page,
|
|
2736
|
+
client,
|
|
2737
|
+
ignoredTargetKeys: deepResearchTargetKeys,
|
|
2738
|
+
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
2739
|
+
minTurnIndex: baselineTurns,
|
|
2740
|
+
onPlan: async (plan) => {
|
|
2741
|
+
researchPlan = plan;
|
|
2742
|
+
await emitRuntimeHint();
|
|
2743
|
+
},
|
|
2744
|
+
});
|
|
2515
2745
|
const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
|
|
2516
2746
|
ignoredTargetKeys: deepResearchTargetKeys,
|
|
2517
2747
|
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
@@ -2550,6 +2780,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2550
2780
|
artifacts: savedArtifacts,
|
|
2551
2781
|
archive,
|
|
2552
2782
|
modelSelection: modelSelectionEvidence,
|
|
2783
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
2553
2784
|
tookMs: durationMs,
|
|
2554
2785
|
answerTokens: tokens,
|
|
2555
2786
|
answerChars: researchResult.text.length,
|
|
@@ -2559,7 +2790,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2559
2790
|
tabUrl: lastUrl,
|
|
2560
2791
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2561
2792
|
promptSubmitted,
|
|
2793
|
+
ownedRecoveryTarget,
|
|
2562
2794
|
controllerPid: process.pid,
|
|
2795
|
+
researchPlan,
|
|
2563
2796
|
};
|
|
2564
2797
|
}
|
|
2565
2798
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
@@ -2572,6 +2805,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2572
2805
|
const deadline = Date.now() + timeoutMs;
|
|
2573
2806
|
while (Date.now() < deadline) {
|
|
2574
2807
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
2808
|
+
throwIfAssistantUiError(snapshot);
|
|
2575
2809
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
2576
2810
|
if (text) {
|
|
2577
2811
|
const normalized = normalizeForComparison(text);
|
|
@@ -2647,6 +2881,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2647
2881
|
tabUrl: lastUrl,
|
|
2648
2882
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2649
2883
|
promptSubmitted,
|
|
2884
|
+
ownedRecoveryTarget,
|
|
2650
2885
|
controllerPid: process.pid,
|
|
2651
2886
|
},
|
|
2652
2887
|
});
|
|
@@ -2705,6 +2940,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2705
2940
|
tabUrl: lastUrl,
|
|
2706
2941
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2707
2942
|
promptSubmitted,
|
|
2943
|
+
ownedRecoveryTarget,
|
|
2708
2944
|
controllerPid: process.pid,
|
|
2709
2945
|
};
|
|
2710
2946
|
throw await createAssistantTimeoutError({
|
|
@@ -2793,6 +3029,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2793
3029
|
let stableCount = 0;
|
|
2794
3030
|
while (Date.now() < deadline) {
|
|
2795
3031
|
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
3032
|
+
throwIfAssistantUiError(snapshot);
|
|
2796
3033
|
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
2797
3034
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
2798
3035
|
if (!isStillEcho) {
|
|
@@ -2887,6 +3124,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2887
3124
|
tabUrl: lastUrl,
|
|
2888
3125
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2889
3126
|
promptSubmitted,
|
|
3127
|
+
ownedRecoveryTarget,
|
|
2890
3128
|
controllerPid: process.pid,
|
|
2891
3129
|
},
|
|
2892
3130
|
}),
|
|
@@ -2949,6 +3187,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2949
3187
|
tabUrl: lastUrl,
|
|
2950
3188
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2951
3189
|
promptSubmitted,
|
|
3190
|
+
ownedRecoveryTarget,
|
|
2952
3191
|
artifacts: savedArtifacts,
|
|
2953
3192
|
generatedImages: imageArtifacts.generatedImages,
|
|
2954
3193
|
savedImages: imageArtifacts.savedImages,
|
|
@@ -2956,10 +3195,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2956
3195
|
savedFiles: fileArtifacts.savedFiles,
|
|
2957
3196
|
archive,
|
|
2958
3197
|
modelSelection: modelSelectionEvidence,
|
|
3198
|
+
thinkingSelection: thinkingSelectionEvidence,
|
|
2959
3199
|
controllerPid: process.pid,
|
|
2960
3200
|
};
|
|
2961
3201
|
}
|
|
2962
3202
|
catch (error) {
|
|
3203
|
+
if (options.signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
3204
|
+
runStatus = "cancelled";
|
|
3205
|
+
throw new BrowserRunCancelledError();
|
|
3206
|
+
}
|
|
2963
3207
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
2964
3208
|
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
2965
3209
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
@@ -2992,62 +3236,66 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2992
3236
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
2993
3237
|
: undefined,
|
|
2994
3238
|
promptSubmitted,
|
|
3239
|
+
ownedRecoveryTarget,
|
|
2995
3240
|
controllerPid: process.pid,
|
|
3241
|
+
researchPlan,
|
|
2996
3242
|
},
|
|
2997
3243
|
});
|
|
2998
3244
|
}
|
|
2999
3245
|
finally {
|
|
3000
|
-
await
|
|
3001
|
-
|
|
3002
|
-
await
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3246
|
+
await withoutBrowserCancellation(async () => {
|
|
3247
|
+
stopThinkingMonitor?.();
|
|
3248
|
+
await conversationUrlMonitor?.stop();
|
|
3249
|
+
removeDialogHandler?.();
|
|
3250
|
+
const keepRemoteBrowser = Boolean(config.keepBrowser);
|
|
3251
|
+
const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
3006
3252
|
runStatus,
|
|
3253
|
+
ownsTarget,
|
|
3254
|
+
keepBrowser: keepRemoteBrowser,
|
|
3255
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
3256
|
+
closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
|
|
3007
3257
|
});
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
return;
|
|
3023
|
-
}
|
|
3024
|
-
const safeToClose = !keepRemoteBrowser ||
|
|
3025
|
-
Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
|
|
3026
|
-
if (!safeToClose) {
|
|
3027
|
-
logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
|
|
3028
|
-
return;
|
|
3029
|
-
}
|
|
3030
|
-
const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
|
|
3031
|
-
if (!closeConfirmed && keepRemoteBrowser) {
|
|
3032
|
-
const replacementTargetId = await createChromePageTarget(port, logger, host);
|
|
3033
|
-
if (!replacementTargetId) {
|
|
3034
|
-
logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
|
|
3258
|
+
const closeConnection = async () => {
|
|
3259
|
+
let preserveTarget = !shouldCloseOwnedRemoteTarget;
|
|
3260
|
+
if (!preserveTarget && keepRemoteBrowser && client && remoteTargetId) {
|
|
3261
|
+
try {
|
|
3262
|
+
const { targetInfos } = await client.Target.getTargets();
|
|
3263
|
+
if (!targetInfos.some((target) => target.type === "page" && target.targetId !== remoteTargetId)) {
|
|
3264
|
+
const replacement = await client.Target.createTarget({ url: "about:blank" });
|
|
3265
|
+
if (!replacement.targetId)
|
|
3266
|
+
preserveTarget = true;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
catch {
|
|
3270
|
+
preserveTarget = true;
|
|
3271
|
+
}
|
|
3035
3272
|
}
|
|
3273
|
+
await closeRemoteConnectionAfterRun({
|
|
3274
|
+
connectionClosedUnexpectedly,
|
|
3275
|
+
connection,
|
|
3276
|
+
client,
|
|
3277
|
+
preserveTarget,
|
|
3278
|
+
});
|
|
3279
|
+
};
|
|
3280
|
+
if (tabLease) {
|
|
3281
|
+
const handle = tabLease;
|
|
3282
|
+
tabLease = null;
|
|
3283
|
+
await handle.release({ onRelease: closeConnection }).catch(async () => {
|
|
3284
|
+
await closeRemoteConnectionAfterRun({
|
|
3285
|
+
connectionClosedUnexpectedly,
|
|
3286
|
+
connection,
|
|
3287
|
+
client,
|
|
3288
|
+
preserveTarget: true,
|
|
3289
|
+
}).catch(() => undefined);
|
|
3290
|
+
});
|
|
3036
3291
|
}
|
|
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`);
|
|
3292
|
+
else {
|
|
3293
|
+
await closeConnection().catch(() => undefined);
|
|
3294
|
+
}
|
|
3295
|
+
// Don't kill remote Chrome - it's not ours to manage
|
|
3296
|
+
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
3297
|
+
logger(`Remote session complete • ${totalSeconds.toFixed(1)}s total`);
|
|
3298
|
+
});
|
|
3051
3299
|
}
|
|
3052
3300
|
}
|
|
3053
3301
|
export { estimateTokenCount } from "./utils.js";
|
|
@@ -3066,10 +3314,13 @@ export const __test__ = {
|
|
|
3066
3314
|
isImageOnlyUiChromeText,
|
|
3067
3315
|
listIgnoredRemoteChromeFlags,
|
|
3068
3316
|
normalizeAuthenticatedModelSelectionError,
|
|
3317
|
+
pollGeneratedImageOrTextAssistantResponse,
|
|
3069
3318
|
resolveManualLoginWaitMs,
|
|
3319
|
+
shouldApplyThinkingTimeSelection,
|
|
3070
3320
|
shouldCleanupBlankTabsAfterLastLease,
|
|
3071
3321
|
shouldCloseOwnedRunTargetAfterRun,
|
|
3072
3322
|
shouldKeepLocalBrowserOpen,
|
|
3323
|
+
releaseLocalBrowserTabLease,
|
|
3073
3324
|
waitForAssistantResponseWithReload,
|
|
3074
3325
|
};
|
|
3075
3326
|
export { syncCookies } from "./cookies.js";
|