@steipete/oracle 0.16.0 → 0.17.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 +72 -337
- package/dist/bin/oracle-cli.js +184 -55
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +410 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +389 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +324 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/scripts/test-browser.js +1 -25
- package/dist/src/browser/actions/modelSelection.js +36 -34
- package/dist/src/browser/actions/navigation.js +8 -3
- package/dist/src/browser/actions/thinkingTime.js +39 -3
- package/dist/src/browser/cdpLiveness.js +78 -0
- package/dist/src/browser/chromeLifecycle.js +85 -39
- package/dist/src/browser/conversationUrl.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +2 -4
- package/dist/src/browser/cookies.js +6 -4
- package/dist/src/browser/index.js +181 -50
- package/dist/src/browser/liveTabs.js +2 -2
- package/dist/src/browser/modelDisplay.js +67 -0
- package/dist/src/browser/reattach.js +14 -1
- package/dist/src/browser/reattachHelpers.js +3 -5
- package/dist/src/browser/reattachability.js +2 -1
- package/dist/src/browser/recoverConversation.js +2 -1
- package/dist/src/browser/sessionRunner.js +9 -10
- package/dist/src/browser/tabLeaseRegistry.js +9 -3
- package/dist/src/browser/wslHost.js +50 -0
- package/dist/src/cli/browserConfig.js +31 -11
- package/dist/src/cli/browserDefaults.js +4 -1
- package/dist/src/cli/detach.js +21 -4
- package/dist/src/cli/dryRun.js +13 -2
- package/dist/src/cli/engine.js +2 -2
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/sessionDisplay.js +60 -8
- package/dist/src/cli/sessionLifecycle.js +2 -1
- package/dist/src/cli/sessionRunner.js +159 -62
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/cli/tui/index.js +12 -4
- package/dist/src/duration.js +3 -0
- package/dist/src/gemini-web/client.js +19 -1
- package/dist/src/oracle/modelResolver.js +8 -1
- package/dist/src/oracle/request.js +9 -2
- package/dist/src/oracle/run.js +43 -3
- package/dist/src/remote/server.js +8 -0
- package/dist/src/sessionManager.js +41 -12
- 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 +16 -16
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -4,7 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
6
|
import { copyChromeProfile } from "./profileCopy.js";
|
|
7
|
-
import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, connectToRemoteChrome, connectWithNewTab, closeTab,
|
|
7
|
+
import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, connectToRemoteChrome, connectWithNewTab, closeTab, createChromePageTarget, ensureChromePageTargetAfterClose, closeBlankChromeTabs, } from "./chromeLifecycle.js";
|
|
8
8
|
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
9
|
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
10
10
|
import { INPUT_SELECTORS } from "./constants.js";
|
|
@@ -19,6 +19,7 @@ import { BrowserAutomationError } from "../oracle/errors.js";
|
|
|
19
19
|
import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
|
|
20
20
|
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
21
21
|
import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
22
|
+
import { connectionLostUserMessage, isRecoverableChromeDisconnect, probeChromeTargetLiveness, } from "./cdpLiveness.js";
|
|
22
23
|
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
23
24
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
24
25
|
import { collectGeneratedImageArtifacts } from "./chatgptImages.js";
|
|
@@ -32,6 +33,7 @@ import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./ac
|
|
|
32
33
|
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
33
34
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
|
|
34
35
|
import { createConversationUrlMonitor, } from "./conversationUrlMonitor.js";
|
|
36
|
+
import { extractStableConversationIdFromUrl as extractConversationIdFromUrl, isStableConversationUrl as isConversationUrl, } from "./conversationUrl.js";
|
|
35
37
|
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
|
|
36
38
|
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
|
|
37
39
|
export { formatThinkingLog, formatThinkingWaitingLog, buildThinkingStatusExpressionForTest, readThinkingStatusForTest, sanitizeThinkingText, startThinkingStatusMonitorForTest, } from "./actions/thinkingStatus.js";
|
|
@@ -69,6 +71,9 @@ function classifyPreservedBrowserError(error, headless) {
|
|
|
69
71
|
function shouldPreserveBrowserOnError(error, headless) {
|
|
70
72
|
return classifyPreservedBrowserError(error, headless) !== null;
|
|
71
73
|
}
|
|
74
|
+
function normalizeAuthenticatedModelSelectionError(error) {
|
|
75
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
76
|
+
}
|
|
72
77
|
function shouldKeepLocalBrowserOpen(options) {
|
|
73
78
|
if (options.usingCopiedProfile)
|
|
74
79
|
return false;
|
|
@@ -535,7 +540,17 @@ async function closeRemoteConnectionAfterRun(options) {
|
|
|
535
540
|
}
|
|
536
541
|
}
|
|
537
542
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
538
|
-
return options.runStatus === "complete" &&
|
|
543
|
+
return (options.runStatus === "complete" &&
|
|
544
|
+
options.ownsTarget &&
|
|
545
|
+
(Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser));
|
|
546
|
+
}
|
|
547
|
+
function shouldCleanupBlankTabsAfterLastLease(options) {
|
|
548
|
+
return (options.runStatus === "complete" &&
|
|
549
|
+
options.ownsTarget &&
|
|
550
|
+
!options.connectionClosedUnexpectedly &&
|
|
551
|
+
options.manualLogin &&
|
|
552
|
+
options.keepBrowser &&
|
|
553
|
+
Boolean(options.chromePort));
|
|
539
554
|
}
|
|
540
555
|
function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
|
|
541
556
|
return {
|
|
@@ -796,8 +811,38 @@ export async function runBrowserMode(options) {
|
|
|
796
811
|
const disconnectPromise = new Promise((_, reject) => {
|
|
797
812
|
client?.on("disconnect", () => {
|
|
798
813
|
connectionClosedUnexpectedly = true;
|
|
799
|
-
|
|
800
|
-
|
|
814
|
+
void (async () => {
|
|
815
|
+
const liveness = await probeChromeTargetLiveness({
|
|
816
|
+
host: chromeHost,
|
|
817
|
+
port: chrome.port,
|
|
818
|
+
targetId: lastTargetId ?? isolatedTargetId,
|
|
819
|
+
});
|
|
820
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
821
|
+
if (recoverable) {
|
|
822
|
+
logger("CDP client disconnected; Chrome/target still reachable. Leaving run recoverable for reattach.");
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
logger("Chrome window closed; attempting to abort run.");
|
|
826
|
+
}
|
|
827
|
+
reject(new BrowserAutomationError(connectionLostUserMessage({ recoverable }), {
|
|
828
|
+
stage: "connection-lost",
|
|
829
|
+
recoverableDisconnect: recoverable,
|
|
830
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
831
|
+
runtime: {
|
|
832
|
+
chromePid: chrome.pid,
|
|
833
|
+
chromePort: chrome.port,
|
|
834
|
+
chromeHost,
|
|
835
|
+
userDataDir,
|
|
836
|
+
chromeTargetId: lastTargetId ?? isolatedTargetId ?? undefined,
|
|
837
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
838
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
839
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
840
|
+
: undefined,
|
|
841
|
+
promptSubmitted,
|
|
842
|
+
controllerPid: process.pid,
|
|
843
|
+
},
|
|
844
|
+
}));
|
|
845
|
+
})();
|
|
801
846
|
});
|
|
802
847
|
});
|
|
803
848
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
@@ -1012,11 +1057,9 @@ export async function runBrowserMode(options) {
|
|
|
1012
1057
|
}
|
|
1013
1058
|
},
|
|
1014
1059
|
})).catch((error) => {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
: "";
|
|
1019
|
-
throw new Error(`${base}${hint}`);
|
|
1060
|
+
// Login has already been verified above. Preserve the picker failure instead of
|
|
1061
|
+
// misdiagnosing an unavailable model as missing cookies.
|
|
1062
|
+
throw normalizeAuthenticatedModelSelectionError(error);
|
|
1020
1063
|
});
|
|
1021
1064
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
1022
1065
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
@@ -1300,7 +1343,11 @@ export async function runBrowserMode(options) {
|
|
|
1300
1343
|
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
1301
1344
|
logger(`[browser] Rechecking assistant response at ${conversationUrl}`);
|
|
1302
1345
|
await raceWithDisconnect(Page.navigate({ url: conversationUrl }));
|
|
1303
|
-
await raceWithDisconnect(
|
|
1346
|
+
await raceWithDisconnect(waitForResumedConversationHydration(Runtime, recheckTimeoutMs || 30_000, logger, {
|
|
1347
|
+
requirePriorTurns: true,
|
|
1348
|
+
requirePromptReady: false,
|
|
1349
|
+
expectedConversationUrl: conversationUrl,
|
|
1350
|
+
}));
|
|
1304
1351
|
}
|
|
1305
1352
|
// Validate session before attempting recheck - sessions can expire during the delay
|
|
1306
1353
|
const sessionValid = await validateChatGPTSession(Runtime, logger);
|
|
@@ -1727,19 +1774,34 @@ export async function runBrowserMode(options) {
|
|
|
1727
1774
|
throw normalizedError;
|
|
1728
1775
|
}
|
|
1729
1776
|
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
1730
|
-
logger(`Chrome
|
|
1777
|
+
logger(`Chrome connection lost before completion: ${normalizedError.message}`);
|
|
1731
1778
|
logger(normalizedError.stack);
|
|
1732
1779
|
}
|
|
1733
1780
|
await emitRuntimeHint();
|
|
1734
|
-
|
|
1781
|
+
if (normalizedError instanceof BrowserAutomationError &&
|
|
1782
|
+
normalizedError.details?.stage === "connection-lost") {
|
|
1783
|
+
throw normalizedError;
|
|
1784
|
+
}
|
|
1785
|
+
const liveness = await probeChromeTargetLiveness({
|
|
1786
|
+
host: chromeHost,
|
|
1787
|
+
port: chrome.port,
|
|
1788
|
+
targetId: lastTargetId ?? isolatedTargetId,
|
|
1789
|
+
});
|
|
1790
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
1791
|
+
throw new BrowserAutomationError(connectionLostUserMessage({ recoverable }), {
|
|
1735
1792
|
stage: "connection-lost",
|
|
1793
|
+
recoverableDisconnect: recoverable,
|
|
1794
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
1736
1795
|
runtime: {
|
|
1737
1796
|
chromePid: chrome.pid,
|
|
1738
1797
|
chromePort: chrome.port,
|
|
1739
1798
|
chromeHost,
|
|
1740
1799
|
userDataDir,
|
|
1741
1800
|
chromeTargetId: lastTargetId,
|
|
1742
|
-
tabUrl: lastUrl,
|
|
1801
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
1802
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
1803
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1804
|
+
: undefined,
|
|
1743
1805
|
promptSubmitted,
|
|
1744
1806
|
controllerPid: process.pid,
|
|
1745
1807
|
},
|
|
@@ -1758,15 +1820,12 @@ export async function runBrowserMode(options) {
|
|
|
1758
1820
|
// Close the isolated tab once the response has been fully captured to prevent
|
|
1759
1821
|
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
1760
1822
|
// so reattach can recover the response.
|
|
1761
|
-
|
|
1823
|
+
const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
1762
1824
|
runStatus,
|
|
1763
1825
|
ownsTarget,
|
|
1764
1826
|
keepBrowser: effectiveKeepBrowser,
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
chrome?.port) {
|
|
1768
|
-
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
1769
|
-
}
|
|
1827
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
1828
|
+
});
|
|
1770
1829
|
let keepBrowserOpen = shouldKeepLocalBrowserOpen({
|
|
1771
1830
|
effectiveKeepBrowser,
|
|
1772
1831
|
preserveBrowserOnError,
|
|
@@ -1784,18 +1843,6 @@ export async function runBrowserMode(options) {
|
|
|
1784
1843
|
}
|
|
1785
1844
|
return otherActiveBrowserTabLeases;
|
|
1786
1845
|
};
|
|
1787
|
-
if (runStatus === "complete" &&
|
|
1788
|
-
manualLogin &&
|
|
1789
|
-
!connectionClosedUnexpectedly &&
|
|
1790
|
-
chrome?.port &&
|
|
1791
|
-
ownsTarget) {
|
|
1792
|
-
const otherLeasesActive = await hasOtherActiveLeases().catch(() => true);
|
|
1793
|
-
if (!otherLeasesActive) {
|
|
1794
|
-
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1795
|
-
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1796
|
-
}).catch(() => undefined);
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
1846
|
if (!keepBrowserOpen && manualLogin && tabLease) {
|
|
1800
1847
|
const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
1801
1848
|
if (cleanupLockTimeoutMs > 0) {
|
|
@@ -1813,10 +1860,55 @@ export async function runBrowserMode(options) {
|
|
|
1813
1860
|
terminatedRecordedChrome = await terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false);
|
|
1814
1861
|
}
|
|
1815
1862
|
}
|
|
1863
|
+
const closeOwnedRunTarget = async () => {
|
|
1864
|
+
if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
const safeToClose = !effectiveKeepBrowser ||
|
|
1868
|
+
Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
|
|
1869
|
+
if (!safeToClose) {
|
|
1870
|
+
logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
|
|
1874
|
+
if (!closeConfirmed && effectiveKeepBrowser) {
|
|
1875
|
+
const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
|
|
1876
|
+
if (!replacementTargetId) {
|
|
1877
|
+
logger(`[browser] Chrome page retention could not be verified after closing ${isolatedTargetId}.`);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
const cleanupBlankTabs = async () => {
|
|
1882
|
+
if (!shouldCleanupBlankTabsAfterLastLease({
|
|
1883
|
+
runStatus,
|
|
1884
|
+
ownsTarget,
|
|
1885
|
+
connectionClosedUnexpectedly,
|
|
1886
|
+
manualLogin,
|
|
1887
|
+
keepBrowser: effectiveKeepBrowser,
|
|
1888
|
+
chromePort: chrome?.port,
|
|
1889
|
+
}) ||
|
|
1890
|
+
!chrome?.port) {
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1894
|
+
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1895
|
+
preserveOneBlank: true,
|
|
1896
|
+
});
|
|
1897
|
+
};
|
|
1816
1898
|
if (tabLease) {
|
|
1817
1899
|
const handle = tabLease;
|
|
1818
1900
|
tabLease = null;
|
|
1819
|
-
|
|
1901
|
+
const onRelease = async ({ isLastLease }) => {
|
|
1902
|
+
await closeOwnedRunTarget();
|
|
1903
|
+
if (isLastLease) {
|
|
1904
|
+
await cleanupBlankTabs();
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
await handle.release({ onRelease }).catch(() => undefined);
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
await closeOwnedRunTarget();
|
|
1911
|
+
await cleanupBlankTabs();
|
|
1820
1912
|
}
|
|
1821
1913
|
removeDialogHandler?.();
|
|
1822
1914
|
removeTerminationHooks?.();
|
|
@@ -2520,7 +2612,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2520
2612
|
lastUrl = conversationUrl;
|
|
2521
2613
|
logger(`[browser] Rechecking assistant response at ${conversationUrl}`);
|
|
2522
2614
|
await Page.navigate({ url: conversationUrl });
|
|
2523
|
-
await
|
|
2615
|
+
await waitForResumedConversationHydration(Runtime, recheckTimeoutMs || 30_000, logger, {
|
|
2616
|
+
requirePriorTurns: true,
|
|
2617
|
+
requirePromptReady: false,
|
|
2618
|
+
expectedConversationUrl: conversationUrl,
|
|
2619
|
+
});
|
|
2524
2620
|
}
|
|
2525
2621
|
// Validate session before attempting recheck - sessions can expire during the delay
|
|
2526
2622
|
const sessionValid = await validateChatGPTSession(Runtime, logger);
|
|
@@ -2869,15 +2965,27 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2869
2965
|
}
|
|
2870
2966
|
throw normalizedError;
|
|
2871
2967
|
}
|
|
2872
|
-
|
|
2968
|
+
const liveness = await probeChromeTargetLiveness({
|
|
2969
|
+
host,
|
|
2970
|
+
port,
|
|
2971
|
+
targetId: remoteTargetId,
|
|
2972
|
+
browserWSEndpoint,
|
|
2973
|
+
});
|
|
2974
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
2975
|
+
throw new BrowserAutomationError(connectionLostUserMessage({ recoverable, remote: true }), {
|
|
2873
2976
|
stage: "connection-lost",
|
|
2977
|
+
recoverableDisconnect: recoverable,
|
|
2978
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
2874
2979
|
runtime: {
|
|
2875
2980
|
chromeHost: host,
|
|
2876
2981
|
chromePort: port,
|
|
2877
2982
|
chromeBrowserWSEndpoint: browserWSEndpoint,
|
|
2878
2983
|
chromeProfileRoot,
|
|
2879
2984
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2880
|
-
tabUrl: lastUrl,
|
|
2985
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
2986
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
2987
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
2988
|
+
: undefined,
|
|
2881
2989
|
promptSubmitted,
|
|
2882
2990
|
controllerPid: process.pid,
|
|
2883
2991
|
},
|
|
@@ -2897,17 +3005,40 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2897
3005
|
// ignore
|
|
2898
3006
|
}
|
|
2899
3007
|
removeDialogHandler?.();
|
|
3008
|
+
const keepRemoteBrowser = Boolean(config.keepBrowser);
|
|
3009
|
+
const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
3010
|
+
runStatus,
|
|
3011
|
+
ownsTarget,
|
|
3012
|
+
keepBrowser: keepRemoteBrowser,
|
|
3013
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
3014
|
+
});
|
|
3015
|
+
const closeOwnedRemoteTarget = async () => {
|
|
3016
|
+
if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
|
|
3017
|
+
return;
|
|
3018
|
+
}
|
|
3019
|
+
const safeToClose = !keepRemoteBrowser ||
|
|
3020
|
+
Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
|
|
3021
|
+
if (!safeToClose) {
|
|
3022
|
+
logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
|
|
3023
|
+
return;
|
|
3024
|
+
}
|
|
3025
|
+
const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
|
|
3026
|
+
if (!closeConfirmed && keepRemoteBrowser) {
|
|
3027
|
+
const replacementTargetId = await createChromePageTarget(port, logger, host);
|
|
3028
|
+
if (!replacementTargetId) {
|
|
3029
|
+
logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
2900
3033
|
if (tabLease) {
|
|
2901
3034
|
const handle = tabLease;
|
|
2902
3035
|
tabLease = null;
|
|
2903
|
-
await handle
|
|
3036
|
+
await handle
|
|
3037
|
+
.release({ onRelease: async () => closeOwnedRemoteTarget() })
|
|
3038
|
+
.catch(() => undefined);
|
|
2904
3039
|
}
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
ownsTarget,
|
|
2908
|
-
keepBrowser: Boolean(config.keepBrowser),
|
|
2909
|
-
})) {
|
|
2910
|
-
await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger);
|
|
3040
|
+
else {
|
|
3041
|
+
await closeOwnedRemoteTarget();
|
|
2911
3042
|
}
|
|
2912
3043
|
// Don't kill remote Chrome - it's not ours to manage
|
|
2913
3044
|
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -2929,9 +3060,12 @@ export const __test__ = {
|
|
|
2929
3060
|
isManualLoginProfileInitialized,
|
|
2930
3061
|
isImageOnlyUiChromeText,
|
|
2931
3062
|
listIgnoredRemoteChromeFlags,
|
|
3063
|
+
normalizeAuthenticatedModelSelectionError,
|
|
2932
3064
|
resolveManualLoginWaitMs,
|
|
3065
|
+
shouldCleanupBlankTabsAfterLastLease,
|
|
2933
3066
|
shouldCloseOwnedRunTargetAfterRun,
|
|
2934
3067
|
shouldKeepLocalBrowserOpen,
|
|
3068
|
+
waitForAssistantResponseWithReload,
|
|
2935
3069
|
};
|
|
2936
3070
|
export { syncCookies } from "./cookies.js";
|
|
2937
3071
|
export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from "./pageActions.js";
|
|
@@ -2963,7 +3097,11 @@ async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logg
|
|
|
2963
3097
|
}
|
|
2964
3098
|
logger("Assistant response stalled; reloading conversation and retrying once");
|
|
2965
3099
|
await Page.navigate({ url: conversationUrl });
|
|
2966
|
-
await
|
|
3100
|
+
await waitForResumedConversationHydration(Runtime, timeoutMs, logger, {
|
|
3101
|
+
requirePriorTurns: true,
|
|
3102
|
+
requirePromptReady: false,
|
|
3103
|
+
expectedConversationUrl: conversationUrl,
|
|
3104
|
+
});
|
|
2967
3105
|
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
2968
3106
|
}
|
|
2969
3107
|
}
|
|
@@ -3129,9 +3267,6 @@ async function readConversationTurnCount(Runtime, logger) {
|
|
|
3129
3267
|
}
|
|
3130
3268
|
return null;
|
|
3131
3269
|
}
|
|
3132
|
-
function isConversationUrl(url) {
|
|
3133
|
-
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
3134
|
-
}
|
|
3135
3270
|
function describeDevtoolsFirewallHint(host, port) {
|
|
3136
3271
|
if (!isWsl())
|
|
3137
3272
|
return null;
|
|
@@ -3152,10 +3287,6 @@ function isWsl() {
|
|
|
3152
3287
|
return true;
|
|
3153
3288
|
return os.release().toLowerCase().includes("microsoft");
|
|
3154
3289
|
}
|
|
3155
|
-
function extractConversationIdFromUrl(url) {
|
|
3156
|
-
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
3157
|
-
return match?.[1];
|
|
3158
|
-
}
|
|
3159
3290
|
async function resolveUserDataBaseDir() {
|
|
3160
3291
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
3161
3292
|
if (isWsl()) {
|
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, INPUT_SELECTORS, MODEL_BUTTON_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTOR, } from "./constants.js";
|
|
4
4
|
import { captureAssistantMarkdown, readAssistantSnapshot } from "./actions/assistantResponse.js";
|
|
5
5
|
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
6
|
+
import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
|
|
6
7
|
import { delay } from "./utils.js";
|
|
7
8
|
export const DEFAULT_REMOTE_CHROME_HOST = "127.0.0.1";
|
|
8
9
|
export const DEFAULT_REMOTE_CHROME_PORT = 9222;
|
|
@@ -481,8 +482,7 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
484
|
export function extractConversationIdFromUrl(url) {
|
|
484
|
-
|
|
485
|
-
return match?.[1] ?? undefined;
|
|
485
|
+
return extractStableConversationIdFromUrl(normalizeUrl(url));
|
|
486
486
|
}
|
|
487
487
|
export function formatBrowserTabState(tab) {
|
|
488
488
|
return tab.state ?? classifyTabState(tab);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function cleanLabel(value) {
|
|
2
|
+
const label = value?.trim();
|
|
3
|
+
return label ? label : null;
|
|
4
|
+
}
|
|
5
|
+
function sameLabel(left, right) {
|
|
6
|
+
return left.localeCompare(right, undefined, { sensitivity: "accent" }) === 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Describe what a browser run will try to select without presenting the target as observed fact.
|
|
10
|
+
*/
|
|
11
|
+
export function formatBrowserModelTarget({ model, desiredModel, modelStrategy, }) {
|
|
12
|
+
const requested = cleanLabel(model) ?? "n/a";
|
|
13
|
+
if (modelStrategy === "current" || modelStrategy === "ignore") {
|
|
14
|
+
return `picker=${modelStrategy}; requested=${requested}`;
|
|
15
|
+
}
|
|
16
|
+
const target = cleanLabel(desiredModel);
|
|
17
|
+
if (!target) {
|
|
18
|
+
return requested;
|
|
19
|
+
}
|
|
20
|
+
return `target=${target}; requested=${requested}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Prefer picker evidence only when Oracle verified it. Otherwise retain the requested CLI key.
|
|
24
|
+
* In particular, a bare `Pro` picker label must not be expanded to a server-side model version.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveBrowserModelDisplayName({ model, evidence, }) {
|
|
27
|
+
const verifiedLabel = evidence?.verified ? cleanLabel(evidence.resolvedLabel) : null;
|
|
28
|
+
return verifiedLabel ?? cleanLabel(model) ?? "n/a";
|
|
29
|
+
}
|
|
30
|
+
export function formatBrowserModelWithRequestedKey(input) {
|
|
31
|
+
const displayName = resolveBrowserModelDisplayName(input);
|
|
32
|
+
const requested = cleanLabel(input.model);
|
|
33
|
+
if (!requested || sameLabel(displayName, requested)) {
|
|
34
|
+
return displayName;
|
|
35
|
+
}
|
|
36
|
+
return `${displayName} (requested ${requested})`;
|
|
37
|
+
}
|
|
38
|
+
export function resolveSessionBrowserModelDisplayName(metadata, model = metadata.model) {
|
|
39
|
+
const sessionModel = cleanLabel(metadata.model);
|
|
40
|
+
const requestedModel = cleanLabel(model);
|
|
41
|
+
const evidenceApplies = requestedModel === null
|
|
42
|
+
? sessionModel === null
|
|
43
|
+
: sessionModel !== null && sameLabel(requestedModel, sessionModel);
|
|
44
|
+
return resolveBrowserModelDisplayName({
|
|
45
|
+
model,
|
|
46
|
+
evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function formatSessionBrowserModelWithRequestedKey(metadata, model = metadata.model) {
|
|
50
|
+
const sessionModel = cleanLabel(metadata.model);
|
|
51
|
+
const requestedModel = cleanLabel(model);
|
|
52
|
+
const evidenceApplies = requestedModel === null
|
|
53
|
+
? sessionModel === null
|
|
54
|
+
: sessionModel !== null && sameLabel(requestedModel, sessionModel);
|
|
55
|
+
return formatBrowserModelWithRequestedKey({
|
|
56
|
+
model,
|
|
57
|
+
evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export function formatBrowserModelSelectionEvidence(evidence, model) {
|
|
61
|
+
const requestedKey = cleanLabel(model) ?? "(none)";
|
|
62
|
+
const target = cleanLabel(evidence.requestedModel) ?? "(none)";
|
|
63
|
+
const resolvedLabel = cleanLabel(evidence.resolvedLabel) ?? "(unavailable)";
|
|
64
|
+
const strategy = evidence.strategy ?? "(default)";
|
|
65
|
+
const verified = evidence.verified ? "yes" : "no";
|
|
66
|
+
return `requestedKey=${requestedKey}; target=${target}; resolvedLabel=${resolvedLabel}; status=${evidence.status}; strategy=${strategy}; verified=${verified}; source=${evidence.source}; capturedAt=${evidence.capturedAt}`;
|
|
67
|
+
}
|
|
@@ -2,7 +2,7 @@ import CDP from "chrome-remote-interface";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
5
|
-
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
|
|
5
|
+
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, } from "./pageActions.js";
|
|
6
6
|
import { launchChrome, connectToChrome, positionChromeWindowOffscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
|
|
7
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
8
8
|
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
@@ -98,6 +98,13 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
98
98
|
const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
|
|
99
99
|
await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
|
|
100
100
|
await ensureConversationOpen();
|
|
101
|
+
const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
|
|
102
|
+
const expectedConversationUrl = buildConversationUrl(runtime, resolveBrowserConfig(config ?? {}).url);
|
|
103
|
+
await waitForHydration(Runtime, timeoutMs, logger, {
|
|
104
|
+
requirePriorTurns: true,
|
|
105
|
+
requirePromptReady: false,
|
|
106
|
+
expectedConversationUrl: expectedConversationUrl ?? undefined,
|
|
107
|
+
});
|
|
101
108
|
const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
|
|
102
109
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
103
110
|
if (config?.researchMode === "deep") {
|
|
@@ -226,6 +233,12 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
226
233
|
}
|
|
227
234
|
await waitForLocationChange(Runtime, 15_000);
|
|
228
235
|
}
|
|
236
|
+
const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
|
|
237
|
+
await waitForHydration(Runtime, resolved.inputTimeoutMs, logger, {
|
|
238
|
+
requirePriorTurns: true,
|
|
239
|
+
requirePromptReady: false,
|
|
240
|
+
expectedConversationUrl: conversationUrl ?? undefined,
|
|
241
|
+
});
|
|
229
242
|
const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
|
|
230
243
|
const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
|
|
231
244
|
const timeoutMs = resolved.timeoutMs ?? 120_000;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
2
|
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
3
|
+
import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
|
|
3
4
|
import { delay } from "./utils.js";
|
|
4
5
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
5
6
|
export function pickTarget(targets, runtime) {
|
|
@@ -29,14 +30,11 @@ export function pickTarget(targets, runtime) {
|
|
|
29
30
|
return targets.find((t) => t.type === "page") ?? targets[0];
|
|
30
31
|
}
|
|
31
32
|
export function extractConversationIdFromUrl(url) {
|
|
32
|
-
|
|
33
|
-
return undefined;
|
|
34
|
-
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
35
|
-
return match?.[1];
|
|
33
|
+
return extractStableConversationIdFromUrl(url);
|
|
36
34
|
}
|
|
37
35
|
export function buildConversationUrl(runtime, baseUrl) {
|
|
38
36
|
if (runtime.tabUrl) {
|
|
39
|
-
if (runtime.tabUrl
|
|
37
|
+
if (extractConversationIdFromUrl(runtime.tabUrl)) {
|
|
40
38
|
return runtime.tabUrl;
|
|
41
39
|
}
|
|
42
40
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStableConversationUrl } from "./conversationUrl.js";
|
|
1
2
|
/**
|
|
2
3
|
* True when the URL points at a specific ChatGPT conversation (`/c/<id>`) on
|
|
3
4
|
* chatgpt.com or chat.openai.com. Rejects home, project shell, and external
|
|
@@ -17,7 +18,7 @@ export function isRecoverableChatGptConversationUrl(candidate) {
|
|
|
17
18
|
if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
20
|
-
return
|
|
21
|
+
return isStableConversationUrl(url.pathname);
|
|
21
22
|
}
|
|
22
23
|
catch {
|
|
23
24
|
return false;
|
|
@@ -67,7 +67,8 @@ export function isRecoveredConversationHarvestReady(harvested) {
|
|
|
67
67
|
(typeof harvested.lastAssistantTurnIndex === "number" &&
|
|
68
68
|
typeof harvested.lastUserTurnIndex === "number" &&
|
|
69
69
|
harvested.lastAssistantTurnIndex > harvested.lastUserTurnIndex);
|
|
70
|
-
|
|
70
|
+
const hasHydratedUserTurn = typeof harvested.lastUserTurnIndex === "number" && harvested.lastUserTurnIndex >= 0;
|
|
71
|
+
return ((harvested.stopExists === true && hasHydratedUserTurn) ||
|
|
71
72
|
((harvested.assistantCount ?? 0) > 0 &&
|
|
72
73
|
assistantFollowsLatestUser &&
|
|
73
74
|
latestAssistant.trim().length > 0 &&
|
|
@@ -5,6 +5,7 @@ import { runBrowserMode } from "../browserMode.js";
|
|
|
5
5
|
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
6
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
7
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
8
|
+
import { formatBrowserModelSelectionEvidence, formatBrowserModelTarget, resolveBrowserModelDisplayName, } from "./modelDisplay.js";
|
|
8
9
|
const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
|
|
9
10
|
const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
|
|
10
11
|
function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
@@ -21,13 +22,6 @@ function buildUnavailableModelSelectionEvidence(browserConfig) {
|
|
|
21
22
|
capturedAt: new Date().toISOString(),
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
|
-
function formatModelSelectionEvidence(evidence) {
|
|
25
|
-
const requested = evidence.requestedModel ?? "(none)";
|
|
26
|
-
const resolved = evidence.resolvedLabel ?? "(unavailable)";
|
|
27
|
-
const strategy = evidence.strategy ?? "(default)";
|
|
28
|
-
const verified = evidence.verified ? "yes" : "no";
|
|
29
|
-
return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
|
|
30
|
-
}
|
|
31
25
|
function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
|
|
32
26
|
const candidates = [
|
|
33
27
|
runOptions.model,
|
|
@@ -87,7 +81,12 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
87
81
|
if (promptArtifacts.bundled) {
|
|
88
82
|
log(chalk.dim(`Packed ${promptArtifacts.bundled.originalCount} files into 1 bundle (contents counted in token estimate).`));
|
|
89
83
|
}
|
|
90
|
-
const
|
|
84
|
+
const launchModel = formatBrowserModelTarget({
|
|
85
|
+
model: runOptions.model,
|
|
86
|
+
desiredModel: browserConfig.desiredModel,
|
|
87
|
+
modelStrategy: browserConfig.modelStrategy,
|
|
88
|
+
});
|
|
89
|
+
const headerLine = `Launching browser mode (${launchModel}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
|
|
91
90
|
const automationLogger = ((message) => {
|
|
92
91
|
if (typeof message !== "string")
|
|
93
92
|
return;
|
|
@@ -150,7 +149,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
150
149
|
}
|
|
151
150
|
const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
|
|
152
151
|
if (modelSelection) {
|
|
153
|
-
log(
|
|
152
|
+
log(`[browser] Model selection evidence: ${formatBrowserModelSelectionEvidence(modelSelection, runOptions.model)}`);
|
|
154
153
|
}
|
|
155
154
|
const warnings = buildBrowserRunWarnings({
|
|
156
155
|
runOptions,
|
|
@@ -199,7 +198,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
199
198
|
})();
|
|
200
199
|
const { line1, line2 } = formatFinishLine({
|
|
201
200
|
elapsedMs: browserResult.tookMs,
|
|
202
|
-
model: `${runOptions.model}[browser]`,
|
|
201
|
+
model: `${resolveBrowserModelDisplayName({ model: runOptions.model, evidence: modelSelection })}[browser]`,
|
|
203
202
|
tokensPart,
|
|
204
203
|
detailParts: [
|
|
205
204
|
runOptions.file && runOptions.file.length > 0 ? `files=${runOptions.file.length}` : null,
|
|
@@ -61,7 +61,7 @@ export async function acquireBrowserTabLease(profileDir, options, deps = {}) {
|
|
|
61
61
|
options.logger?.(`[browser] Acquired ChatGPT browser slot ${leaseId.slice(0, 8)} (${maxConcurrentTabs} max).`);
|
|
62
62
|
return {
|
|
63
63
|
id: leaseId,
|
|
64
|
-
release: async () => releaseBrowserTabLease(profileDir, leaseId, options.logger),
|
|
64
|
+
release: async (releaseOptions) => releaseBrowserTabLease(profileDir, leaseId, options.logger, releaseOptions),
|
|
65
65
|
update: async (patch) => updateBrowserTabLease(profileDir, leaseId, patch),
|
|
66
66
|
};
|
|
67
67
|
}
|
|
@@ -86,11 +86,17 @@ export async function updateBrowserTabLease(profileDir, leaseId, patch) {
|
|
|
86
86
|
await writeRegistry(profileDir, { version: 1, leases });
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
export async function releaseBrowserTabLease(profileDir, leaseId, logger) {
|
|
89
|
+
export async function releaseBrowserTabLease(profileDir, leaseId, logger, options = {}) {
|
|
90
90
|
await withRegistryLock(profileDir, async () => {
|
|
91
91
|
const registry = await readRegistry(profileDir);
|
|
92
|
-
const
|
|
92
|
+
const active = pruneStaleLeases(registry.leases, {
|
|
93
|
+
nowMs: Date.now(),
|
|
94
|
+
staleMs: DEFAULT_STALE_MS,
|
|
95
|
+
isProcessAlive,
|
|
96
|
+
});
|
|
97
|
+
const leases = active.filter((lease) => lease.id !== leaseId);
|
|
93
98
|
await writeRegistry(profileDir, { version: 1, leases });
|
|
99
|
+
await options.onRelease?.({ isLastLease: leases.length === 0 });
|
|
94
100
|
}).catch(() => undefined);
|
|
95
101
|
logger?.(`[browser] Released ChatGPT browser slot ${leaseId.slice(0, 8)}.`);
|
|
96
102
|
}
|