@steipete/oracle 0.16.0 → 0.16.1
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 +0 -0
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/cdpLiveness.js +78 -0
- package/dist/src/browser/chromeLifecycle.js +83 -2
- 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 +158 -42
- package/dist/src/browser/liveTabs.js +2 -2
- package/dist/src/browser/reattachHelpers.js +3 -5
- package/dist/src/browser/reattachability.js +2 -1
- package/dist/src/browser/tabLeaseRegistry.js +9 -3
- package/dist/src/cli/browserDefaults.js +4 -1
- package/dist/src/cli/sessionRunner.js +49 -2
- package/dist/src/remote/server.js +8 -0
- 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 +8 -8
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
package/README.md
CHANGED
|
@@ -294,7 +294,7 @@ Browser automation can open or control Chrome, so dry-runs and live runs print a
|
|
|
294
294
|
| `--followup-model <model>` | For multi-model OpenAI/Azure parent sessions, choose which model response to continue from. |
|
|
295
295
|
| `--base-url <url>` | Point API runs at LiteLLM/Azure/OpenRouter/etc. |
|
|
296
296
|
| `--chatgpt-url <url>` | Target a ChatGPT workspace/folder or Temporary Chat URL (browser). |
|
|
297
|
-
| `--browser-model-strategy <select\|current\|ignore>` | Control ChatGPT model selection in browser mode
|
|
297
|
+
| `--browser-model-strategy <select\|current\|ignore>` | Control ChatGPT model selection in browser mode. Explicit `current` keeps the active model without inheriting configured thinking time; pass `--browser-thinking-time` to change effort. `ignore` skips the picker. |
|
|
298
298
|
| `--browser-manual-login` | Skip cookie copy; reuse a persistent automation profile and wait for manual ChatGPT login. |
|
|
299
299
|
| `--browser-attach-running` | Reuse your current local browser session through local `DevToolsActivePort` discovery; Oracle opens a dedicated tab instead of launching Chrome (defaults to `127.0.0.1:9222`, or combine with `--remote-chrome <host:port>` to hint a different local endpoint). |
|
|
300
300
|
| `--browser-tab <ref>` | Reuse an existing ChatGPT tab by `current`, target id, URL, or title substring instead of opening a new tab. |
|
package/dist/bin/oracle-cli.js
CHANGED
|
File without changes
|
package/dist/bin/oracle-mcp.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { listRemoteChromeTargets } from "./chromeLifecycle.js";
|
|
2
|
+
import { verifyDevToolsReachable } from "./profileState.js";
|
|
3
|
+
/**
|
|
4
|
+
* Probe whether Chrome's DevTools endpoint (and optionally a specific target)
|
|
5
|
+
* is still reachable after a CDP client WebSocket disconnect.
|
|
6
|
+
*/
|
|
7
|
+
export async function probeChromeTargetLiveness(options) {
|
|
8
|
+
const host = options.host || "127.0.0.1";
|
|
9
|
+
const port = options.port;
|
|
10
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
11
|
+
return { endpointReachable: false, targetFound: null, error: "missing debug port" };
|
|
12
|
+
}
|
|
13
|
+
const verifyEndpoint = options.verifyEndpoint ?? verifyDevToolsReachable;
|
|
14
|
+
const endpoint = await verifyEndpoint({ host, port, attempts: 2, timeoutMs: 1500 });
|
|
15
|
+
if (!endpoint.ok) {
|
|
16
|
+
return {
|
|
17
|
+
endpointReachable: false,
|
|
18
|
+
targetFound: null,
|
|
19
|
+
error: endpoint.error,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const targetId = options.targetId?.trim();
|
|
23
|
+
if (!targetId) {
|
|
24
|
+
return { endpointReachable: true, targetFound: null };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const listTargets = options.listTargets ?? listRemoteChromeTargets;
|
|
28
|
+
const targets = await listTargets({
|
|
29
|
+
host,
|
|
30
|
+
port,
|
|
31
|
+
browserWSEndpoint: options.browserWSEndpoint,
|
|
32
|
+
});
|
|
33
|
+
const match = targets.find((target) => {
|
|
34
|
+
const id = target.targetId ?? target.id;
|
|
35
|
+
return id === targetId;
|
|
36
|
+
});
|
|
37
|
+
if (!match) {
|
|
38
|
+
return { endpointReachable: true, targetFound: false };
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
endpointReachable: true,
|
|
42
|
+
targetFound: true,
|
|
43
|
+
matchedUrl: typeof match.url === "string" ? match.url : undefined,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
// Endpoint answered /json/version; treat list failures as still recoverable.
|
|
49
|
+
return { endpointReachable: true, targetFound: null, error: message };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function isRecoverableChromeDisconnect(liveness) {
|
|
53
|
+
if (!liveness.endpointReachable) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// Confirmed live target → recoverable.
|
|
57
|
+
if (liveness.targetFound === true) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Confirmed missing target → not recoverable.
|
|
61
|
+
if (liveness.targetFound === false) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// targetFound === null:
|
|
65
|
+
// - no target id was provided (endpoint-only check) → recoverable
|
|
66
|
+
// - target list failed after a specific id was requested (error set) → fail closed
|
|
67
|
+
return !liveness.error;
|
|
68
|
+
}
|
|
69
|
+
export function connectionLostUserMessage(options) {
|
|
70
|
+
if (options.recoverable) {
|
|
71
|
+
return options.remote
|
|
72
|
+
? "Remote Chrome DevTools client disconnected before oracle finished; the browser target appears still alive."
|
|
73
|
+
: "Chrome DevTools client disconnected before oracle finished; the browser target appears still alive.";
|
|
74
|
+
}
|
|
75
|
+
return options.remote
|
|
76
|
+
? "Remote Chrome connection lost before Oracle finished."
|
|
77
|
+
: "Chrome window closed before oracle finished. Please keep it open until completion.";
|
|
78
|
+
}
|
|
@@ -406,13 +406,79 @@ export async function closeTab(port, targetId, logger, host) {
|
|
|
406
406
|
const effectiveHost = host ?? "127.0.0.1";
|
|
407
407
|
try {
|
|
408
408
|
await CDP.Close({ host: effectiveHost, port, id: targetId });
|
|
409
|
-
|
|
409
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
410
|
+
await delay(25);
|
|
411
|
+
let targets;
|
|
412
|
+
try {
|
|
413
|
+
targets = (await CDP.List({ host: effectiveHost, port }));
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (!targets.some((target) => (target.targetId ?? target.id) === targetId)) {
|
|
419
|
+
logger(`Closed isolated browser tab (target=${targetId})`);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
logger(`Browser tab close was not confirmed (target=${targetId})`);
|
|
424
|
+
return false;
|
|
410
425
|
}
|
|
411
426
|
catch (error) {
|
|
427
|
+
try {
|
|
428
|
+
const targets = (await CDP.List({ host: effectiveHost, port }));
|
|
429
|
+
if (!targets.some((target) => (target.targetId ?? target.id) === targetId)) {
|
|
430
|
+
logger(`Closed isolated browser tab (target=${targetId})`);
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
// Preserve the original close error below.
|
|
436
|
+
}
|
|
412
437
|
const message = error instanceof Error ? error.message : String(error);
|
|
413
438
|
logger(`Failed to close browser tab ${targetId}: ${message}`);
|
|
439
|
+
return false;
|
|
414
440
|
}
|
|
415
441
|
}
|
|
442
|
+
export async function createChromePageTarget(port, logger, host) {
|
|
443
|
+
const effectiveHost = host ?? "127.0.0.1";
|
|
444
|
+
try {
|
|
445
|
+
const created = (await CDP.New({
|
|
446
|
+
host: effectiveHost,
|
|
447
|
+
port,
|
|
448
|
+
url: "about:blank",
|
|
449
|
+
}));
|
|
450
|
+
const createdTargetId = created.targetId ?? created.id;
|
|
451
|
+
if (!createdTargetId) {
|
|
452
|
+
logger("Failed to create a replacement Chrome tab.");
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
logger(`Opened replacement Chrome tab (target=${createdTargetId})`);
|
|
456
|
+
return createdTargetId;
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
460
|
+
logger(`Failed to create a replacement Chrome tab: ${message}`);
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
export async function ensureChromePageTargetAfterClose(port, closingTargetId, logger, host) {
|
|
465
|
+
const effectiveHost = host ?? "127.0.0.1";
|
|
466
|
+
try {
|
|
467
|
+
const targets = (await CDP.List({ host: effectiveHost, port }));
|
|
468
|
+
const existingPageTargetId = targets
|
|
469
|
+
.filter((target) => target.type === "page")
|
|
470
|
+
.map((target) => target.targetId ?? target.id)
|
|
471
|
+
.find((targetId) => Boolean(targetId) && targetId !== closingTargetId);
|
|
472
|
+
if (existingPageTargetId) {
|
|
473
|
+
return existingPageTargetId;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
478
|
+
logger(`Failed to inspect Chrome tabs before closing ${closingTargetId}: ${message}`);
|
|
479
|
+
}
|
|
480
|
+
return await createChromePageTarget(port, logger, host);
|
|
481
|
+
}
|
|
416
482
|
export async function closeBlankChromeTabs(port, logger, host, options) {
|
|
417
483
|
const effectiveHost = host ?? "127.0.0.1";
|
|
418
484
|
const excluded = new Set([...(options?.excludeTargetIds ?? [])].filter((targetId) => typeof targetId === "string" && targetId.length > 0));
|
|
@@ -425,10 +491,20 @@ export async function closeBlankChromeTabs(port, logger, host, options) {
|
|
|
425
491
|
logger(`Failed to inspect blank Chrome tabs: ${message}`);
|
|
426
492
|
return;
|
|
427
493
|
}
|
|
494
|
+
const preservedBlankTargetId = options?.preserveOneBlank
|
|
495
|
+
? targets
|
|
496
|
+
.filter(isBlankPageTarget)
|
|
497
|
+
.map((target) => target.targetId ?? target.id)
|
|
498
|
+
.filter((targetId) => Boolean(targetId))
|
|
499
|
+
.sort()[0]
|
|
500
|
+
: undefined;
|
|
428
501
|
let closed = 0;
|
|
429
502
|
for (const target of targets) {
|
|
430
503
|
const targetId = target.targetId ?? target.id;
|
|
431
|
-
if (!targetId ||
|
|
504
|
+
if (!targetId ||
|
|
505
|
+
targetId === preservedBlankTargetId ||
|
|
506
|
+
excluded.has(targetId) ||
|
|
507
|
+
!isBlankPageTarget(target)) {
|
|
432
508
|
continue;
|
|
433
509
|
}
|
|
434
510
|
try {
|
|
@@ -487,6 +563,11 @@ function buildChromeFlags(headless, debugBindAddress, hideWindow = false) {
|
|
|
487
563
|
// off-screen avoids desktop disruption while preserving normal rendering.
|
|
488
564
|
flags.push("--window-position=-32000,-32000");
|
|
489
565
|
}
|
|
566
|
+
// Opt-in only: container/CI Chromium often cannot use the sandbox. Callers must
|
|
567
|
+
// set ORACLE_CHROME_NO_SANDBOX=1 explicitly (never default this on).
|
|
568
|
+
if (process.env.ORACLE_CHROME_NO_SANDBOX === "1") {
|
|
569
|
+
flags.push("--no-sandbox", "--disable-dev-shm-usage");
|
|
570
|
+
}
|
|
490
571
|
return flags;
|
|
491
572
|
}
|
|
492
573
|
export function buildChromeFlagsForTest(headless, debugBindAddress, hideWindow = false) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const CONVERSATION_ID_PATH = /\/c\/([a-zA-Z0-9-]+)(?=[/?#]|$)/;
|
|
2
|
+
/**
|
|
3
|
+
* Extract a durable ChatGPT conversation id from a URL.
|
|
4
|
+
*
|
|
5
|
+
* ChatGPT can briefly expose client-created routes such as `/c/WEB:<request-id>`
|
|
6
|
+
* before replacing them with the persisted conversation URL. Those transient
|
|
7
|
+
* routes must not be used to scope assistant-response capture or reattachment.
|
|
8
|
+
*/
|
|
9
|
+
export function extractStableConversationIdFromUrl(url) {
|
|
10
|
+
if (!url)
|
|
11
|
+
return undefined;
|
|
12
|
+
return url.match(CONVERSATION_ID_PATH)?.[1];
|
|
13
|
+
}
|
|
14
|
+
export function isStableConversationUrl(url) {
|
|
15
|
+
return extractStableConversationIdFromUrl(url) !== undefined;
|
|
16
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { delay } from "./utils.js";
|
|
2
|
+
import { isStableConversationUrl } from "./conversationUrl.js";
|
|
2
3
|
export function createConversationUrlMonitor(options) {
|
|
3
4
|
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
4
5
|
const wait = options.wait ?? delay;
|
|
@@ -14,7 +15,7 @@ export function createConversationUrlMonitor(options) {
|
|
|
14
15
|
if (stopped) {
|
|
15
16
|
return false;
|
|
16
17
|
}
|
|
17
|
-
if (url &&
|
|
18
|
+
if (url && isStableConversationUrl(url)) {
|
|
18
19
|
options.logger(`[browser] conversation url (${label}) = ${url}`);
|
|
19
20
|
const persist = options.persistUrl(url);
|
|
20
21
|
activePersists.add(persist);
|
|
@@ -59,6 +60,3 @@ export function createConversationUrlMonitor(options) {
|
|
|
59
60
|
},
|
|
60
61
|
};
|
|
61
62
|
}
|
|
62
|
-
function isConversationUrl(url) {
|
|
63
|
-
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
64
|
-
}
|
|
@@ -244,12 +244,14 @@ function normalizeExpiration(expires) {
|
|
|
244
244
|
if (value <= 0) {
|
|
245
245
|
return undefined;
|
|
246
246
|
}
|
|
247
|
-
|
|
248
|
-
|
|
247
|
+
// Units by magnitude (do not treat Unix seconds ~1.7e9 as milliseconds):
|
|
248
|
+
// - >= 1e15: Chrome/WebKit FILETIME microseconds since 1601
|
|
249
|
+
// - >= 1e12: Unix milliseconds
|
|
250
|
+
// - else: Unix seconds (sweet-cookie / Chromium Cookie.expires)
|
|
251
|
+
if (value >= 1_000_000_000_000_000) {
|
|
249
252
|
return Math.round(value / 1_000_000 - 11644473600);
|
|
250
253
|
}
|
|
251
|
-
if (value
|
|
252
|
-
// Likely milliseconds; normalize to seconds for CDP.
|
|
254
|
+
if (value >= 1_000_000_000_000) {
|
|
253
255
|
return Math.round(value / 1000);
|
|
254
256
|
}
|
|
255
257
|
return Math.round(value);
|
|
@@ -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";
|
|
@@ -535,7 +537,17 @@ async function closeRemoteConnectionAfterRun(options) {
|
|
|
535
537
|
}
|
|
536
538
|
}
|
|
537
539
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
538
|
-
return options.runStatus === "complete" &&
|
|
540
|
+
return (options.runStatus === "complete" &&
|
|
541
|
+
options.ownsTarget &&
|
|
542
|
+
(Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser));
|
|
543
|
+
}
|
|
544
|
+
function shouldCleanupBlankTabsAfterLastLease(options) {
|
|
545
|
+
return (options.runStatus === "complete" &&
|
|
546
|
+
options.ownsTarget &&
|
|
547
|
+
!options.connectionClosedUnexpectedly &&
|
|
548
|
+
options.manualLogin &&
|
|
549
|
+
options.keepBrowser &&
|
|
550
|
+
Boolean(options.chromePort));
|
|
539
551
|
}
|
|
540
552
|
function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
|
|
541
553
|
return {
|
|
@@ -796,8 +808,38 @@ export async function runBrowserMode(options) {
|
|
|
796
808
|
const disconnectPromise = new Promise((_, reject) => {
|
|
797
809
|
client?.on("disconnect", () => {
|
|
798
810
|
connectionClosedUnexpectedly = true;
|
|
799
|
-
|
|
800
|
-
|
|
811
|
+
void (async () => {
|
|
812
|
+
const liveness = await probeChromeTargetLiveness({
|
|
813
|
+
host: chromeHost,
|
|
814
|
+
port: chrome.port,
|
|
815
|
+
targetId: lastTargetId ?? isolatedTargetId,
|
|
816
|
+
});
|
|
817
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
818
|
+
if (recoverable) {
|
|
819
|
+
logger("CDP client disconnected; Chrome/target still reachable. Leaving run recoverable for reattach.");
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
logger("Chrome window closed; attempting to abort run.");
|
|
823
|
+
}
|
|
824
|
+
reject(new BrowserAutomationError(connectionLostUserMessage({ recoverable }), {
|
|
825
|
+
stage: "connection-lost",
|
|
826
|
+
recoverableDisconnect: recoverable,
|
|
827
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
828
|
+
runtime: {
|
|
829
|
+
chromePid: chrome.pid,
|
|
830
|
+
chromePort: chrome.port,
|
|
831
|
+
chromeHost,
|
|
832
|
+
userDataDir,
|
|
833
|
+
chromeTargetId: lastTargetId ?? isolatedTargetId ?? undefined,
|
|
834
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
835
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
836
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
837
|
+
: undefined,
|
|
838
|
+
promptSubmitted,
|
|
839
|
+
controllerPid: process.pid,
|
|
840
|
+
},
|
|
841
|
+
}));
|
|
842
|
+
})();
|
|
801
843
|
});
|
|
802
844
|
});
|
|
803
845
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
@@ -1727,19 +1769,34 @@ export async function runBrowserMode(options) {
|
|
|
1727
1769
|
throw normalizedError;
|
|
1728
1770
|
}
|
|
1729
1771
|
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
1730
|
-
logger(`Chrome
|
|
1772
|
+
logger(`Chrome connection lost before completion: ${normalizedError.message}`);
|
|
1731
1773
|
logger(normalizedError.stack);
|
|
1732
1774
|
}
|
|
1733
1775
|
await emitRuntimeHint();
|
|
1734
|
-
|
|
1776
|
+
if (normalizedError instanceof BrowserAutomationError &&
|
|
1777
|
+
normalizedError.details?.stage === "connection-lost") {
|
|
1778
|
+
throw normalizedError;
|
|
1779
|
+
}
|
|
1780
|
+
const liveness = await probeChromeTargetLiveness({
|
|
1781
|
+
host: chromeHost,
|
|
1782
|
+
port: chrome.port,
|
|
1783
|
+
targetId: lastTargetId ?? isolatedTargetId,
|
|
1784
|
+
});
|
|
1785
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
1786
|
+
throw new BrowserAutomationError(connectionLostUserMessage({ recoverable }), {
|
|
1735
1787
|
stage: "connection-lost",
|
|
1788
|
+
recoverableDisconnect: recoverable,
|
|
1789
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
1736
1790
|
runtime: {
|
|
1737
1791
|
chromePid: chrome.pid,
|
|
1738
1792
|
chromePort: chrome.port,
|
|
1739
1793
|
chromeHost,
|
|
1740
1794
|
userDataDir,
|
|
1741
1795
|
chromeTargetId: lastTargetId,
|
|
1742
|
-
tabUrl: lastUrl,
|
|
1796
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
1797
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
1798
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1799
|
+
: undefined,
|
|
1743
1800
|
promptSubmitted,
|
|
1744
1801
|
controllerPid: process.pid,
|
|
1745
1802
|
},
|
|
@@ -1758,15 +1815,12 @@ export async function runBrowserMode(options) {
|
|
|
1758
1815
|
// Close the isolated tab once the response has been fully captured to prevent
|
|
1759
1816
|
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
1760
1817
|
// so reattach can recover the response.
|
|
1761
|
-
|
|
1818
|
+
const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
1762
1819
|
runStatus,
|
|
1763
1820
|
ownsTarget,
|
|
1764
1821
|
keepBrowser: effectiveKeepBrowser,
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
chrome?.port) {
|
|
1768
|
-
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
1769
|
-
}
|
|
1822
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
1823
|
+
});
|
|
1770
1824
|
let keepBrowserOpen = shouldKeepLocalBrowserOpen({
|
|
1771
1825
|
effectiveKeepBrowser,
|
|
1772
1826
|
preserveBrowserOnError,
|
|
@@ -1784,18 +1838,6 @@ export async function runBrowserMode(options) {
|
|
|
1784
1838
|
}
|
|
1785
1839
|
return otherActiveBrowserTabLeases;
|
|
1786
1840
|
};
|
|
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
1841
|
if (!keepBrowserOpen && manualLogin && tabLease) {
|
|
1800
1842
|
const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
1801
1843
|
if (cleanupLockTimeoutMs > 0) {
|
|
@@ -1813,10 +1855,55 @@ export async function runBrowserMode(options) {
|
|
|
1813
1855
|
terminatedRecordedChrome = await terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false);
|
|
1814
1856
|
}
|
|
1815
1857
|
}
|
|
1858
|
+
const closeOwnedRunTarget = async () => {
|
|
1859
|
+
if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const safeToClose = !effectiveKeepBrowser ||
|
|
1863
|
+
Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
|
|
1864
|
+
if (!safeToClose) {
|
|
1865
|
+
logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
|
|
1869
|
+
if (!closeConfirmed && effectiveKeepBrowser) {
|
|
1870
|
+
const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
|
|
1871
|
+
if (!replacementTargetId) {
|
|
1872
|
+
logger(`[browser] Chrome page retention could not be verified after closing ${isolatedTargetId}.`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
const cleanupBlankTabs = async () => {
|
|
1877
|
+
if (!shouldCleanupBlankTabsAfterLastLease({
|
|
1878
|
+
runStatus,
|
|
1879
|
+
ownsTarget,
|
|
1880
|
+
connectionClosedUnexpectedly,
|
|
1881
|
+
manualLogin,
|
|
1882
|
+
keepBrowser: effectiveKeepBrowser,
|
|
1883
|
+
chromePort: chrome?.port,
|
|
1884
|
+
}) ||
|
|
1885
|
+
!chrome?.port) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1889
|
+
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1890
|
+
preserveOneBlank: true,
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
1816
1893
|
if (tabLease) {
|
|
1817
1894
|
const handle = tabLease;
|
|
1818
1895
|
tabLease = null;
|
|
1819
|
-
|
|
1896
|
+
const onRelease = async ({ isLastLease }) => {
|
|
1897
|
+
await closeOwnedRunTarget();
|
|
1898
|
+
if (isLastLease) {
|
|
1899
|
+
await cleanupBlankTabs();
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
await handle.release({ onRelease }).catch(() => undefined);
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
await closeOwnedRunTarget();
|
|
1906
|
+
await cleanupBlankTabs();
|
|
1820
1907
|
}
|
|
1821
1908
|
removeDialogHandler?.();
|
|
1822
1909
|
removeTerminationHooks?.();
|
|
@@ -2869,15 +2956,27 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2869
2956
|
}
|
|
2870
2957
|
throw normalizedError;
|
|
2871
2958
|
}
|
|
2872
|
-
|
|
2959
|
+
const liveness = await probeChromeTargetLiveness({
|
|
2960
|
+
host,
|
|
2961
|
+
port,
|
|
2962
|
+
targetId: remoteTargetId,
|
|
2963
|
+
browserWSEndpoint,
|
|
2964
|
+
});
|
|
2965
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
2966
|
+
throw new BrowserAutomationError(connectionLostUserMessage({ recoverable, remote: true }), {
|
|
2873
2967
|
stage: "connection-lost",
|
|
2968
|
+
recoverableDisconnect: recoverable,
|
|
2969
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
2874
2970
|
runtime: {
|
|
2875
2971
|
chromeHost: host,
|
|
2876
2972
|
chromePort: port,
|
|
2877
2973
|
chromeBrowserWSEndpoint: browserWSEndpoint,
|
|
2878
2974
|
chromeProfileRoot,
|
|
2879
2975
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2880
|
-
tabUrl: lastUrl,
|
|
2976
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
2977
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
2978
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
2979
|
+
: undefined,
|
|
2881
2980
|
promptSubmitted,
|
|
2882
2981
|
controllerPid: process.pid,
|
|
2883
2982
|
},
|
|
@@ -2897,17 +2996,40 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2897
2996
|
// ignore
|
|
2898
2997
|
}
|
|
2899
2998
|
removeDialogHandler?.();
|
|
2999
|
+
const keepRemoteBrowser = Boolean(config.keepBrowser);
|
|
3000
|
+
const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
3001
|
+
runStatus,
|
|
3002
|
+
ownsTarget,
|
|
3003
|
+
keepBrowser: keepRemoteBrowser,
|
|
3004
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
3005
|
+
});
|
|
3006
|
+
const closeOwnedRemoteTarget = async () => {
|
|
3007
|
+
if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const safeToClose = !keepRemoteBrowser ||
|
|
3011
|
+
Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
|
|
3012
|
+
if (!safeToClose) {
|
|
3013
|
+
logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
|
|
3017
|
+
if (!closeConfirmed && keepRemoteBrowser) {
|
|
3018
|
+
const replacementTargetId = await createChromePageTarget(port, logger, host);
|
|
3019
|
+
if (!replacementTargetId) {
|
|
3020
|
+
logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
2900
3024
|
if (tabLease) {
|
|
2901
3025
|
const handle = tabLease;
|
|
2902
3026
|
tabLease = null;
|
|
2903
|
-
await handle
|
|
3027
|
+
await handle
|
|
3028
|
+
.release({ onRelease: async () => closeOwnedRemoteTarget() })
|
|
3029
|
+
.catch(() => undefined);
|
|
2904
3030
|
}
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
ownsTarget,
|
|
2908
|
-
keepBrowser: Boolean(config.keepBrowser),
|
|
2909
|
-
})) {
|
|
2910
|
-
await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger);
|
|
3031
|
+
else {
|
|
3032
|
+
await closeOwnedRemoteTarget();
|
|
2911
3033
|
}
|
|
2912
3034
|
// Don't kill remote Chrome - it's not ours to manage
|
|
2913
3035
|
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -2930,6 +3052,7 @@ export const __test__ = {
|
|
|
2930
3052
|
isImageOnlyUiChromeText,
|
|
2931
3053
|
listIgnoredRemoteChromeFlags,
|
|
2932
3054
|
resolveManualLoginWaitMs,
|
|
3055
|
+
shouldCleanupBlankTabsAfterLastLease,
|
|
2933
3056
|
shouldCloseOwnedRunTargetAfterRun,
|
|
2934
3057
|
shouldKeepLocalBrowserOpen,
|
|
2935
3058
|
};
|
|
@@ -3129,9 +3252,6 @@ async function readConversationTurnCount(Runtime, logger) {
|
|
|
3129
3252
|
}
|
|
3130
3253
|
return null;
|
|
3131
3254
|
}
|
|
3132
|
-
function isConversationUrl(url) {
|
|
3133
|
-
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
3134
|
-
}
|
|
3135
3255
|
function describeDevtoolsFirewallHint(host, port) {
|
|
3136
3256
|
if (!isWsl())
|
|
3137
3257
|
return null;
|
|
@@ -3152,10 +3272,6 @@ function isWsl() {
|
|
|
3152
3272
|
return true;
|
|
3153
3273
|
return os.release().toLowerCase().includes("microsoft");
|
|
3154
3274
|
}
|
|
3155
|
-
function extractConversationIdFromUrl(url) {
|
|
3156
|
-
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
3157
|
-
return match?.[1];
|
|
3158
|
-
}
|
|
3159
3275
|
async function resolveUserDataBaseDir() {
|
|
3160
3276
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
3161
3277
|
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);
|
|
@@ -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;
|
|
@@ -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
|
}
|
|
@@ -11,6 +11,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
11
11
|
};
|
|
12
12
|
const attachRunningRequested = options.browserAttachRunning === true ||
|
|
13
13
|
(isUnset("browserAttachRunning") && browser.attachRunning === true);
|
|
14
|
+
const currentModelRequestedByCli = options.browserModelStrategy === "current" && getSource("browserModelStrategy") === "cli";
|
|
14
15
|
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
15
16
|
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
16
17
|
if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
@@ -89,7 +90,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
89
90
|
if (isUnset("browserModelStrategy") && browser.modelStrategy !== undefined) {
|
|
90
91
|
options.browserModelStrategy = browser.modelStrategy;
|
|
91
92
|
}
|
|
92
|
-
if (
|
|
93
|
+
if (!currentModelRequestedByCli &&
|
|
94
|
+
isUnset("browserThinkingTime") &&
|
|
95
|
+
browser.thinkingTime !== undefined) {
|
|
93
96
|
options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
|
|
94
97
|
}
|
|
95
98
|
if (isUnset("browserResearch") && browser.researchMode !== undefined) {
|
|
@@ -469,6 +469,41 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
469
469
|
response: { status: "running", incompleteReason: "chrome-disconnected" },
|
|
470
470
|
});
|
|
471
471
|
logBrowserReattachGuidance(recoverableRuntime);
|
|
472
|
+
// Only auto-reattach when liveness classified the target as still alive.
|
|
473
|
+
// Closed-Chrome disconnects stay running + guidance but must not enter a
|
|
474
|
+
// futile resume loop (fail closed on availability).
|
|
475
|
+
const recoverableDisconnect = userError.details
|
|
476
|
+
?.recoverableDisconnect === true;
|
|
477
|
+
if (!recoverableDisconnect) {
|
|
478
|
+
log(dim("Skipping auto-reattach: disconnect classified as non-recoverable."));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Connection-lost should attempt the same recovery path as assistant-timeout.
|
|
482
|
+
// When auto-reattach interval is unset, still try a single resume so a live
|
|
483
|
+
// Chrome/target can be harvested instead of leaving the session permanently running.
|
|
484
|
+
const configuredIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
485
|
+
const connectionLostIntervalMs = configuredIntervalMs > 0
|
|
486
|
+
? configuredIntervalMs
|
|
487
|
+
: Math.max(1_000, Math.min(browserConfig?.timeoutMs ?? 30_000, 30_000));
|
|
488
|
+
const success = await autoReattachUntilComplete({
|
|
489
|
+
sessionMeta,
|
|
490
|
+
runtime: recoverableRuntime ?? undefined,
|
|
491
|
+
browserConfig: {
|
|
492
|
+
...browserConfig,
|
|
493
|
+
autoReattachIntervalMs: connectionLostIntervalMs,
|
|
494
|
+
autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
|
|
495
|
+
autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
|
|
496
|
+
},
|
|
497
|
+
browserMetadata: currentBrowser,
|
|
498
|
+
runOptions,
|
|
499
|
+
modelForStatus,
|
|
500
|
+
notificationSettings,
|
|
501
|
+
log,
|
|
502
|
+
maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
|
|
503
|
+
});
|
|
504
|
+
if (success) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
472
507
|
return;
|
|
473
508
|
}
|
|
474
509
|
if (assistantTimeout && mode === "browser" && browserCanReattach) {
|
|
@@ -833,7 +868,7 @@ async function writeAssistantOutput(targetPath, content, log) {
|
|
|
833
868
|
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
834
869
|
}
|
|
835
870
|
}
|
|
836
|
-
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, }) {
|
|
871
|
+
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, }) {
|
|
837
872
|
if (!runtime || !browserConfig) {
|
|
838
873
|
log(dim("Auto-reattach disabled: missing runtime or browser config."));
|
|
839
874
|
return false;
|
|
@@ -848,11 +883,19 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
848
883
|
120_000;
|
|
849
884
|
const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
|
|
850
885
|
const maxDeadline = Date.now() + maxTotalMs;
|
|
886
|
+
const attemptLimit = typeof maxAttempts === "number" && maxAttempts > 0
|
|
887
|
+
? Math.floor(maxAttempts)
|
|
888
|
+
: Number.POSITIVE_INFINITY;
|
|
851
889
|
if (delayMs > 0) {
|
|
852
890
|
log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
|
|
853
891
|
await wait(delayMs);
|
|
854
892
|
}
|
|
855
|
-
|
|
893
|
+
if (Number.isFinite(attemptLimit)) {
|
|
894
|
+
log(dim(`Auto-reattach will try up to ${attemptLimit} attempt(s).`));
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
log(dim(`Auto-reattach will stop after ${formatElapsed(maxTotalMs)} if no answer is captured.`));
|
|
898
|
+
}
|
|
856
899
|
const logger = ((message) => {
|
|
857
900
|
if (message) {
|
|
858
901
|
log(dim(message));
|
|
@@ -943,6 +986,10 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
943
986
|
const message = error instanceof Error ? error.message : String(error);
|
|
944
987
|
log(dim(`Auto-reattach attempt ${attempt} failed: ${message}`));
|
|
945
988
|
}
|
|
989
|
+
if (attempt >= attemptLimit) {
|
|
990
|
+
log(dim(`Auto-reattach stopped after ${attempt} attempt(s) without capturing an answer.`));
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
946
993
|
const remainingAfterAttemptMs = maxDeadline - Date.now();
|
|
947
994
|
if (remainingAfterAttemptMs <= 0) {
|
|
948
995
|
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
@@ -189,6 +189,9 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
});
|
|
191
191
|
automationLogger.verbose = Boolean(payload.options.verbose);
|
|
192
|
+
// Preserve an explicit request to leave the completed conversation tab
|
|
193
|
+
// open before the service forces `keepBrowser` for process lifetime.
|
|
194
|
+
const clientRequestedKeepBrowser = payload.browserConfig?.keepBrowser === true;
|
|
192
195
|
// Remote runs always rely on the host's own Chrome profile; ignore any inline cookie transfer.
|
|
193
196
|
if (payload.browserConfig) {
|
|
194
197
|
payload.browserConfig.inlineCookies = null;
|
|
@@ -212,6 +215,11 @@ export async function createRemoteServer(options = {}, deps = {}) {
|
|
|
212
215
|
attachments,
|
|
213
216
|
fallbackSubmission,
|
|
214
217
|
config: payload.browserConfig,
|
|
218
|
+
// `keepBrowser` above preserves the authenticated shared Chrome
|
|
219
|
+
// process. This separate service policy closes only a successfully
|
|
220
|
+
// captured tab owned by this run, preventing one renderer leak per
|
|
221
|
+
// request while incomplete/reattachable tabs remain untouched.
|
|
222
|
+
closeOwnedTabOnComplete: Boolean(options.manualLoginDefault && !clientRequestedKeepBrowser),
|
|
215
223
|
log: automationLogger,
|
|
216
224
|
heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
|
|
217
225
|
verbose: payload.options.verbose,
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.6 Sol, GPT-5.6, GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://askoracle.sh",
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
61
|
-
"@google/genai": "^2.
|
|
61
|
+
"@google/genai": "^2.13.0",
|
|
62
62
|
"@google/generative-ai": "^0.24.1",
|
|
63
63
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
64
64
|
"@steipete/sweet-cookie": "^0.4.0",
|
|
65
65
|
"chalk": "^5.6.2",
|
|
66
66
|
"chrome-launcher": "^1.2.1",
|
|
67
67
|
"chrome-remote-interface": "^0.34.0",
|
|
68
|
-
"clipboardy": "^5.3.
|
|
68
|
+
"clipboardy": "^5.3.2",
|
|
69
69
|
"commander": "^15.0.0",
|
|
70
70
|
"dotenv": "^17.4.2",
|
|
71
71
|
"fast-glob": "^3.3.3",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"json5": "^2.2.3",
|
|
75
75
|
"kleur": "^4.1.5",
|
|
76
76
|
"markdansi": "0.3.2",
|
|
77
|
-
"openai": "^6.
|
|
77
|
+
"openai": "^6.48.0",
|
|
78
78
|
"osc-progress": "^0.3.2",
|
|
79
79
|
"qs": "^6.15.3",
|
|
80
80
|
"shiki": "^4.3.1",
|
|
@@ -88,13 +88,13 @@
|
|
|
88
88
|
"@types/inquirer": "^9.0.10",
|
|
89
89
|
"@types/node": "^26.1.1",
|
|
90
90
|
"@vitest/coverage-v8": "4.1.10",
|
|
91
|
-
"devtools-protocol": "0.0.
|
|
91
|
+
"devtools-protocol": "0.0.1666840",
|
|
92
92
|
"es-toolkit": "^1.49.0",
|
|
93
93
|
"esbuild": "^0.28.1",
|
|
94
|
-
"oxfmt": "0.
|
|
95
|
-
"oxlint": "^1.
|
|
94
|
+
"oxfmt": "0.60.0",
|
|
95
|
+
"oxlint": "^1.75.0",
|
|
96
96
|
"puppeteer-core": "^25.3.0",
|
|
97
|
-
"tsx": "^4.23.
|
|
97
|
+
"tsx": "^4.23.1",
|
|
98
98
|
"typescript": "^7.0.2",
|
|
99
99
|
"vitest": "^4.1.10"
|
|
100
100
|
},
|
|
Binary file
|
|
Binary file
|