@steipete/oracle 0.8.6 → 0.10.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/LICENSE +1 -1
- package/README.md +130 -45
- package/dist/bin/oracle-cli.js +613 -379
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +314 -104
- package/dist/src/browser/actions/navigation.js +161 -136
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +452 -303
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +17 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
- package/dist/src/browser/providers/index.js +2 -0
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +65 -45
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +7 -4
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +11 -0
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +12 -8
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +145 -87
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +37 -25
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +182 -79
- package/dist/src/cli/sessionLineage.js +60 -0
- package/dist/src/cli/sessionRunner.js +118 -90
- package/dist/src/cli/sessionTable.js +28 -24
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +140 -127
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +80 -0
- package/dist/src/gemini-web/client.js +81 -64
- package/dist/src/gemini-web/executionMode.js +16 -0
- package/dist/src/gemini-web/executor.js +327 -169
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +81 -64
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +84 -46
- package/dist/src/oracle/config.js +124 -58
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +69 -45
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -30
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +23 -15
- package/dist/src/oracle/run.js +172 -140
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +81 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +69 -65
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/README.md +2 -0
- 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/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/profileSync.js +0 -141
- /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
|
@@ -1,26 +1,86 @@
|
|
|
1
|
-
import { mkdtemp, rm, mkdir } from
|
|
2
|
-
import path from
|
|
3
|
-
import os from
|
|
4
|
-
import net from
|
|
5
|
-
import { resolveBrowserConfig } from
|
|
6
|
-
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, closeRemoteChromeTarget, connectWithNewTab, closeTab, } from
|
|
7
|
-
import { syncCookies } from
|
|
8
|
-
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, ensureModelSelection,
|
|
9
|
-
import { INPUT_SELECTORS } from
|
|
10
|
-
import { uploadAttachmentViaDataTransfer } from
|
|
11
|
-
import { ensureThinkingTime } from
|
|
12
|
-
import { estimateTokenCount, withRetries, delay } from
|
|
13
|
-
import { formatElapsed } from
|
|
14
|
-
import { CHATGPT_URL, CONVERSATION_TURN_SELECTOR, DEFAULT_MODEL_STRATEGY } from
|
|
15
|
-
import { BrowserAutomationError } from
|
|
16
|
-
import { alignPromptEchoPair, buildPromptEchoMatcher } from
|
|
17
|
-
import { cleanupStaleProfileState, acquireProfileRunLock, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from
|
|
18
|
-
|
|
19
|
-
|
|
1
|
+
import { mkdtemp, rm, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import net from "node:net";
|
|
5
|
+
import { resolveBrowserConfig } from "./config.js";
|
|
6
|
+
import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToRemoteChrome, closeRemoteChromeTarget, connectWithNewTab, closeTab, } from "./chromeLifecycle.js";
|
|
7
|
+
import { syncCookies } from "./cookies.js";
|
|
8
|
+
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
9
|
+
import { INPUT_SELECTORS } from "./constants.js";
|
|
10
|
+
import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
|
|
11
|
+
import { ensureThinkingTime } from "./actions/thinkingTime.js";
|
|
12
|
+
import { estimateTokenCount, withRetries, delay } from "./utils.js";
|
|
13
|
+
import { formatElapsed } from "../oracle/format.js";
|
|
14
|
+
import { CHATGPT_URL, CONVERSATION_TURN_SELECTOR, DEFAULT_MODEL_STRATEGY } from "./constants.js";
|
|
15
|
+
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
16
|
+
import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
|
|
17
|
+
import { cleanupStaleProfileState, acquireProfileRunLock, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
18
|
+
import { runProviderSubmissionFlow } from "./providerDomFlow.js";
|
|
19
|
+
import { chatgptDomProvider } from "./providers/index.js";
|
|
20
|
+
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
|
|
21
|
+
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
|
|
22
|
+
function redactBrowserConfigForDebugLog(config) {
|
|
23
|
+
const redacted = { ...config };
|
|
24
|
+
if (Array.isArray(config.inlineCookies)) {
|
|
25
|
+
redacted.inlineCookies = `[redacted:${config.inlineCookies.length} cookies]`;
|
|
26
|
+
redacted.inlineCookieCount = config.inlineCookies.length;
|
|
27
|
+
}
|
|
28
|
+
return redacted;
|
|
29
|
+
}
|
|
30
|
+
export function redactBrowserConfigForDebugLogForTest(config) {
|
|
31
|
+
return redactBrowserConfigForDebugLog(config);
|
|
32
|
+
}
|
|
33
|
+
function isCloudflareChallengeError(error) {
|
|
34
|
+
if (!(error instanceof BrowserAutomationError))
|
|
35
|
+
return false;
|
|
36
|
+
return error.details?.stage === "cloudflare-challenge";
|
|
37
|
+
}
|
|
38
|
+
function shouldPreserveBrowserOnError(error, headless) {
|
|
39
|
+
return !headless && isCloudflareChallengeError(error);
|
|
40
|
+
}
|
|
41
|
+
export function shouldPreserveBrowserOnErrorForTest(error, headless) {
|
|
42
|
+
return shouldPreserveBrowserOnError(error, headless);
|
|
43
|
+
}
|
|
44
|
+
function hasBrowserErrorCode(error, code) {
|
|
45
|
+
return (error instanceof BrowserAutomationError &&
|
|
46
|
+
error.details?.code === code);
|
|
47
|
+
}
|
|
48
|
+
async function runSubmissionWithRecovery({ prompt, attachments, fallbackSubmission, submit, reloadPromptComposer, prepareFallbackSubmission, logger, }) {
|
|
49
|
+
let currentPrompt = prompt;
|
|
50
|
+
let currentAttachments = attachments;
|
|
51
|
+
let retriedDeadComposer = false;
|
|
52
|
+
let usedFallbackSubmission = false;
|
|
53
|
+
while (true) {
|
|
54
|
+
try {
|
|
55
|
+
return await submit(currentPrompt, currentAttachments);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const isDeadComposer = hasBrowserErrorCode(error, "dead-composer");
|
|
59
|
+
if (isDeadComposer && !retriedDeadComposer) {
|
|
60
|
+
retriedDeadComposer = true;
|
|
61
|
+
await reloadPromptComposer();
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const isPromptTooLarge = hasBrowserErrorCode(error, "prompt-too-large");
|
|
65
|
+
if (fallbackSubmission && isPromptTooLarge && !usedFallbackSubmission) {
|
|
66
|
+
usedFallbackSubmission = true;
|
|
67
|
+
logger("[browser] Inline prompt too large; retrying with file uploads.");
|
|
68
|
+
await prepareFallbackSubmission();
|
|
69
|
+
currentPrompt = fallbackSubmission.prompt;
|
|
70
|
+
currentAttachments = fallbackSubmission.attachments;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function runSubmissionWithRecoveryForTest(args) {
|
|
78
|
+
return runSubmissionWithRecovery(args);
|
|
79
|
+
}
|
|
20
80
|
export async function runBrowserMode(options) {
|
|
21
81
|
const promptText = options.prompt?.trim();
|
|
22
82
|
if (!promptText) {
|
|
23
|
-
throw new Error(
|
|
83
|
+
throw new Error("Prompt text is required when using browser mode.");
|
|
24
84
|
}
|
|
25
85
|
const attachments = options.attachments ?? [];
|
|
26
86
|
const fallbackSubmission = options.fallbackSubmission;
|
|
@@ -58,9 +118,9 @@ export async function runBrowserMode(options) {
|
|
|
58
118
|
logger(`Failed to persist runtime hint: ${message}`);
|
|
59
119
|
}
|
|
60
120
|
};
|
|
61
|
-
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
121
|
+
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
62
122
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
63
|
-
...config,
|
|
123
|
+
...redactBrowserConfigForDebugLog(config),
|
|
64
124
|
promptLength: promptText.length,
|
|
65
125
|
})}`);
|
|
66
126
|
}
|
|
@@ -76,18 +136,18 @@ export async function runBrowserMode(options) {
|
|
|
76
136
|
if (config.remoteChrome) {
|
|
77
137
|
// Warn about ignored local-only options
|
|
78
138
|
if (config.headless || config.hideWindow || config.keepBrowser || config.chromePath) {
|
|
79
|
-
logger(
|
|
80
|
-
|
|
139
|
+
logger("Note: --remote-chrome ignores local Chrome flags " +
|
|
140
|
+
"(--browser-headless, --browser-hide-window, --browser-keep-browser, --browser-chrome-path).");
|
|
81
141
|
}
|
|
82
142
|
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
83
143
|
}
|
|
84
144
|
const manualLogin = Boolean(config.manualLogin);
|
|
85
145
|
const manualProfileDir = config.manualLoginProfileDir
|
|
86
146
|
? path.resolve(config.manualLoginProfileDir)
|
|
87
|
-
: path.join(os.homedir(),
|
|
147
|
+
: path.join(os.homedir(), ".oracle", "browser-profile");
|
|
88
148
|
const userDataDir = manualLogin
|
|
89
149
|
? manualProfileDir
|
|
90
|
-
: await mkdtemp(path.join(await resolveUserDataBaseDir(),
|
|
150
|
+
: await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
|
|
91
151
|
if (manualLogin) {
|
|
92
152
|
// Learned: manual login reuses a persistent profile so cookies/SSO survive.
|
|
93
153
|
await mkdir(userDataDir, { recursive: true });
|
|
@@ -98,14 +158,16 @@ export async function runBrowserMode(options) {
|
|
|
98
158
|
}
|
|
99
159
|
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
100
160
|
const reusedChrome = manualLogin
|
|
101
|
-
? await maybeReuseRunningChrome(userDataDir, logger, {
|
|
161
|
+
? await maybeReuseRunningChrome(userDataDir, logger, {
|
|
162
|
+
waitForPortMs: config.reuseChromeWaitMs,
|
|
163
|
+
})
|
|
102
164
|
: null;
|
|
103
165
|
const chrome = reusedChrome ??
|
|
104
166
|
(await launchChrome({
|
|
105
167
|
...config,
|
|
106
168
|
remoteChrome: config.remoteChrome,
|
|
107
169
|
}, userDataDir, logger));
|
|
108
|
-
const chromeHost = chrome.host ??
|
|
170
|
+
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
109
171
|
// Persist profile state so future manual-login runs can reuse this Chrome.
|
|
110
172
|
if (manualLogin && chrome.port) {
|
|
111
173
|
await writeDevToolsActivePort(userDataDir, chrome.port);
|
|
@@ -116,7 +178,7 @@ export async function runBrowserMode(options) {
|
|
|
116
178
|
let removeTerminationHooks = null;
|
|
117
179
|
try {
|
|
118
180
|
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
119
|
-
isInFlight: () => runStatus !==
|
|
181
|
+
isInFlight: () => runStatus !== "complete",
|
|
120
182
|
emitRuntimeHint,
|
|
121
183
|
preserveUserDataDir: manualLogin,
|
|
122
184
|
});
|
|
@@ -127,18 +189,19 @@ export async function runBrowserMode(options) {
|
|
|
127
189
|
let client = null;
|
|
128
190
|
let isolatedTargetId = null;
|
|
129
191
|
const startedAt = Date.now();
|
|
130
|
-
let answerText =
|
|
131
|
-
let answerMarkdown =
|
|
132
|
-
let answerHtml =
|
|
133
|
-
let runStatus =
|
|
192
|
+
let answerText = "";
|
|
193
|
+
let answerMarkdown = "";
|
|
194
|
+
let answerHtml = "";
|
|
195
|
+
let runStatus = "attempted";
|
|
134
196
|
let connectionClosedUnexpectedly = false;
|
|
135
197
|
let stopThinkingMonitor = null;
|
|
136
198
|
let removeDialogHandler = null;
|
|
137
199
|
let appliedCookies = 0;
|
|
200
|
+
let preserveBrowserOnError = false;
|
|
138
201
|
try {
|
|
139
202
|
try {
|
|
140
203
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
141
|
-
const connection = await connectWithNewTab(chrome.port, logger,
|
|
204
|
+
const connection = await connectWithNewTab(chrome.port, logger, config.url, chromeHost, {
|
|
142
205
|
fallbackToDefault: !strictTabIsolation,
|
|
143
206
|
retries: strictTabIsolation ? 3 : 0,
|
|
144
207
|
retryDelayMs: 500,
|
|
@@ -154,10 +217,10 @@ export async function runBrowserMode(options) {
|
|
|
154
217
|
throw error;
|
|
155
218
|
}
|
|
156
219
|
const disconnectPromise = new Promise((_, reject) => {
|
|
157
|
-
client?.on(
|
|
220
|
+
client?.on("disconnect", () => {
|
|
158
221
|
connectionClosedUnexpectedly = true;
|
|
159
|
-
logger(
|
|
160
|
-
reject(new Error(
|
|
222
|
+
logger("Chrome window closed; attempting to abort run.");
|
|
223
|
+
reject(new Error("Chrome window closed before oracle finished. Please keep it open until completion."));
|
|
161
224
|
});
|
|
162
225
|
});
|
|
163
226
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
@@ -166,7 +229,7 @@ export async function runBrowserMode(options) {
|
|
|
166
229
|
await hideChromeWindow(chrome, logger);
|
|
167
230
|
}
|
|
168
231
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
169
|
-
if (DOM && typeof DOM.enable ===
|
|
232
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
170
233
|
domainEnablers.push(DOM.enable());
|
|
171
234
|
}
|
|
172
235
|
await Promise.all(domainEnablers);
|
|
@@ -178,13 +241,13 @@ export async function runBrowserMode(options) {
|
|
|
178
241
|
const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
|
|
179
242
|
if (cookieSyncEnabled) {
|
|
180
243
|
if (manualLoginCookieSync) {
|
|
181
|
-
logger(
|
|
244
|
+
logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
|
|
182
245
|
}
|
|
183
246
|
if (!config.inlineCookies) {
|
|
184
|
-
logger(
|
|
247
|
+
logger("Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.");
|
|
185
248
|
}
|
|
186
249
|
else {
|
|
187
|
-
logger(
|
|
250
|
+
logger("Applying inline cookies (skipping Chrome profile read and Keychain prompt)");
|
|
188
251
|
}
|
|
189
252
|
// Learned: always sync cookies before the first navigation so /backend-api/me succeeds.
|
|
190
253
|
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, {
|
|
@@ -196,32 +259,32 @@ export async function runBrowserMode(options) {
|
|
|
196
259
|
});
|
|
197
260
|
appliedCookies = cookieCount;
|
|
198
261
|
if (config.inlineCookies && cookieCount === 0) {
|
|
199
|
-
throw new Error(
|
|
262
|
+
throw new Error("No inline cookies were applied; aborting before navigation.");
|
|
200
263
|
}
|
|
201
264
|
logger(cookieCount > 0
|
|
202
265
|
? config.inlineCookies
|
|
203
266
|
? `Applied ${cookieCount} inline cookies`
|
|
204
|
-
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ??
|
|
267
|
+
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? "Default"}`
|
|
205
268
|
: config.inlineCookies
|
|
206
|
-
?
|
|
207
|
-
:
|
|
269
|
+
? "No inline cookies applied; continuing without session reuse"
|
|
270
|
+
: "No Chrome cookies found; continuing without session reuse");
|
|
208
271
|
}
|
|
209
272
|
else {
|
|
210
273
|
logger(manualLogin
|
|
211
|
-
?
|
|
212
|
-
:
|
|
274
|
+
? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
|
|
275
|
+
: "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
|
|
213
276
|
}
|
|
214
277
|
if (cookieSyncEnabled && !manualLogin && (appliedCookies ?? 0) === 0 && !config.inlineCookies) {
|
|
215
278
|
// Learned: if the profile has no ChatGPT cookies, browser mode will just bounce to login.
|
|
216
279
|
// Fail early so the user knows to sign in.
|
|
217
|
-
throw new BrowserAutomationError(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
stage:
|
|
280
|
+
throw new BrowserAutomationError("No ChatGPT cookies were applied from your Chrome profile; cannot proceed in browser mode. " +
|
|
281
|
+
"Make sure ChatGPT is signed in in the selected profile, use --browser-manual-login / inline cookies, " +
|
|
282
|
+
"or retry with --browser-cookie-wait 5s if Keychain prompts are slow.", {
|
|
283
|
+
stage: "execute-browser",
|
|
221
284
|
details: {
|
|
222
|
-
profile: config.chromeProfile ??
|
|
285
|
+
profile: config.chromeProfile ?? "Default",
|
|
223
286
|
cookiePath: config.chromeCookiePath ?? null,
|
|
224
|
-
hint:
|
|
287
|
+
hint: "If macOS Keychain prompts or denies access, run oracle from a GUI session or use --copy/--render for the manual flow.",
|
|
225
288
|
},
|
|
226
289
|
});
|
|
227
290
|
}
|
|
@@ -231,7 +294,13 @@ export async function runBrowserMode(options) {
|
|
|
231
294
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, baseUrl, logger));
|
|
232
295
|
await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
|
|
233
296
|
// Learned: login checks must happen on the base domain before jumping into project URLs.
|
|
234
|
-
await raceWithDisconnect(waitForLogin({
|
|
297
|
+
await raceWithDisconnect(waitForLogin({
|
|
298
|
+
runtime: Runtime,
|
|
299
|
+
logger,
|
|
300
|
+
appliedCookies,
|
|
301
|
+
manualLogin,
|
|
302
|
+
timeoutMs: config.timeoutMs,
|
|
303
|
+
}));
|
|
235
304
|
if (config.url !== baseUrl) {
|
|
236
305
|
await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
|
|
237
306
|
url: config.url,
|
|
@@ -258,10 +327,10 @@ export async function runBrowserMode(options) {
|
|
|
258
327
|
}
|
|
259
328
|
try {
|
|
260
329
|
const { result } = await Runtime.evaluate({
|
|
261
|
-
expression:
|
|
330
|
+
expression: "location.href",
|
|
262
331
|
returnByValue: true,
|
|
263
332
|
});
|
|
264
|
-
if (typeof result?.value ===
|
|
333
|
+
if (typeof result?.value === "string") {
|
|
265
334
|
lastUrl = result.value;
|
|
266
335
|
}
|
|
267
336
|
}
|
|
@@ -272,7 +341,7 @@ export async function runBrowserMode(options) {
|
|
|
272
341
|
logger(`[browser] url = ${lastUrl}`);
|
|
273
342
|
}
|
|
274
343
|
if (chrome?.port) {
|
|
275
|
-
const suffix = lastTargetId ? ` target=${lastTargetId}` :
|
|
344
|
+
const suffix = lastTargetId ? ` target=${lastTargetId}` : "";
|
|
276
345
|
if (lastUrl) {
|
|
277
346
|
logger(`[reattach] chrome port=${chrome.port} host=${chromeHost} url=${lastUrl}${suffix}`);
|
|
278
347
|
}
|
|
@@ -290,8 +359,11 @@ export async function runBrowserMode(options) {
|
|
|
290
359
|
const start = Date.now();
|
|
291
360
|
while (Date.now() - start < timeoutMs) {
|
|
292
361
|
try {
|
|
293
|
-
const { result } = await Runtime.evaluate({
|
|
294
|
-
|
|
362
|
+
const { result } = await Runtime.evaluate({
|
|
363
|
+
expression: "location.href",
|
|
364
|
+
returnByValue: true,
|
|
365
|
+
});
|
|
366
|
+
if (typeof result?.value === "string" && result.value.includes("/c/")) {
|
|
295
367
|
lastUrl = result.value;
|
|
296
368
|
logger(`[browser] conversation url (${label}) = ${lastUrl}`);
|
|
297
369
|
await emitRuntimeHint();
|
|
@@ -319,7 +391,7 @@ export async function runBrowserMode(options) {
|
|
|
319
391
|
};
|
|
320
392
|
await captureRuntimeSnapshot();
|
|
321
393
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
322
|
-
if (config.desiredModel && modelStrategy !==
|
|
394
|
+
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
323
395
|
await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
324
396
|
retries: 2,
|
|
325
397
|
delayMs: 300,
|
|
@@ -331,15 +403,15 @@ export async function runBrowserMode(options) {
|
|
|
331
403
|
})).catch((error) => {
|
|
332
404
|
const base = error instanceof Error ? error.message : String(error);
|
|
333
405
|
const hint = appliedCookies === 0
|
|
334
|
-
?
|
|
335
|
-
:
|
|
406
|
+
? " No cookies were applied; log in to ChatGPT in Chrome or provide inline cookies (--browser-inline-cookies[(-file)] or ORACLE_BROWSER_COOKIES_JSON)."
|
|
407
|
+
: "";
|
|
336
408
|
throw new Error(`${base}${hint}`);
|
|
337
409
|
});
|
|
338
410
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
339
411
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
340
412
|
}
|
|
341
|
-
else if (modelStrategy ===
|
|
342
|
-
logger(
|
|
413
|
+
else if (modelStrategy === "ignore") {
|
|
414
|
+
logger("Model picker: skipped (strategy=ignore)");
|
|
343
415
|
}
|
|
344
416
|
// Handle thinking time selection if specified
|
|
345
417
|
const thinkingTime = config.thinkingTime;
|
|
@@ -373,13 +445,12 @@ export async function runBrowserMode(options) {
|
|
|
373
445
|
};
|
|
374
446
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
375
447
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
376
|
-
const baselineAssistantText = typeof baselineSnapshot?.text ===
|
|
448
|
+
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
377
449
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
378
|
-
let attachmentWaitTimedOut = false;
|
|
379
450
|
let inputOnlyAttachments = false;
|
|
380
451
|
if (submissionAttachments.length > 0) {
|
|
381
452
|
if (!DOM) {
|
|
382
|
-
throw new Error(
|
|
453
|
+
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
383
454
|
}
|
|
384
455
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
385
456
|
for (let attachmentIndex = 0; attachmentIndex < submissionAttachments.length; attachmentIndex += 1) {
|
|
@@ -395,103 +466,104 @@ export async function runBrowserMode(options) {
|
|
|
395
466
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
396
467
|
const perFileTimeout = 20_000;
|
|
397
468
|
const waitBudget = Math.max(baseTimeout, 45_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
logger('All attachments uploaded');
|
|
401
|
-
}
|
|
402
|
-
catch (error) {
|
|
403
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
404
|
-
if (/Attachments did not finish uploading before timeout/i.test(message)) {
|
|
405
|
-
attachmentWaitTimedOut = true;
|
|
406
|
-
logger(`[browser] Attachment upload timed out after ${Math.round(waitBudget / 1000)}s; continuing without confirmation.`);
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
throw error;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
469
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
470
|
+
logger("All attachments uploaded");
|
|
412
471
|
}
|
|
413
472
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
414
473
|
// Learned: return baselineTurns so assistant polling can ignore earlier content.
|
|
415
|
-
const
|
|
416
|
-
const committedTurns = await submitPrompt({
|
|
474
|
+
const providerState = {
|
|
417
475
|
runtime: Runtime,
|
|
418
476
|
input: Input,
|
|
419
|
-
|
|
420
|
-
|
|
477
|
+
logger,
|
|
478
|
+
timeoutMs: config.timeoutMs,
|
|
421
479
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
480
|
+
baselineTurns: baselineTurns ?? undefined,
|
|
481
|
+
attachmentNames,
|
|
482
|
+
};
|
|
483
|
+
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
484
|
+
prompt,
|
|
485
|
+
evaluate: async () => undefined,
|
|
486
|
+
delay,
|
|
487
|
+
log: logger,
|
|
488
|
+
state: providerState,
|
|
489
|
+
});
|
|
490
|
+
const providerBaselineTurns = providerState.baselineTurns;
|
|
491
|
+
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
492
|
+
baselineTurns = providerBaselineTurns;
|
|
427
493
|
}
|
|
428
494
|
if (attachmentNames.length > 0) {
|
|
429
|
-
if (
|
|
430
|
-
logger(
|
|
431
|
-
}
|
|
432
|
-
else if (inputOnlyAttachments) {
|
|
433
|
-
logger('Attachment UI did not render before send; skipping user-turn attachment verification.');
|
|
495
|
+
if (inputOnlyAttachments) {
|
|
496
|
+
logger("Attachment UI did not render before send; skipping user-turn attachment verification.");
|
|
434
497
|
}
|
|
435
498
|
else {
|
|
436
|
-
const verified = await waitForUserTurnAttachments(Runtime, attachmentNames, 20_000, logger
|
|
499
|
+
const verified = await waitForUserTurnAttachments(Runtime, attachmentNames, 20_000, logger, {
|
|
500
|
+
minTurnIndex: baselineTurns ?? undefined,
|
|
501
|
+
expectedPrompt: prompt,
|
|
502
|
+
expectedConversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
503
|
+
});
|
|
437
504
|
if (!verified) {
|
|
438
|
-
|
|
505
|
+
logger("Sent user message did not expose attachment UI; continuing after upload check.");
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
logger("Verified attachments present on sent user message");
|
|
439
509
|
}
|
|
440
|
-
logger('Verified attachments present on sent user message');
|
|
441
510
|
}
|
|
442
511
|
}
|
|
443
512
|
// Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
|
|
444
|
-
scheduleConversationHint(
|
|
513
|
+
scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
|
|
445
514
|
return { baselineTurns, baselineAssistantText };
|
|
446
515
|
};
|
|
516
|
+
const reloadPromptComposer = async () => {
|
|
517
|
+
logger("[browser] Composer became unresponsive; reloading page and retrying once.");
|
|
518
|
+
await raceWithDisconnect(Page.reload({ ignoreCache: true }));
|
|
519
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
520
|
+
};
|
|
447
521
|
let baselineTurns = null;
|
|
448
522
|
let baselineAssistantText = null;
|
|
449
523
|
await acquireProfileLockIfNeeded();
|
|
450
524
|
try {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
error.details?.code === 'prompt-too-large';
|
|
459
|
-
if (fallbackSubmission && isPromptTooLarge) {
|
|
460
|
-
// Learned: when prompts truncate, retry with file uploads so the UI receives the full content.
|
|
461
|
-
logger('[browser] Inline prompt too large; retrying with file uploads.');
|
|
525
|
+
const submission = await runSubmissionWithRecovery({
|
|
526
|
+
prompt: promptText,
|
|
527
|
+
attachments,
|
|
528
|
+
fallbackSubmission,
|
|
529
|
+
submit: (submissionPrompt, submissionAttachments) => raceWithDisconnect(submitOnce(submissionPrompt, submissionAttachments)),
|
|
530
|
+
reloadPromptComposer,
|
|
531
|
+
prepareFallbackSubmission: async () => {
|
|
462
532
|
await raceWithDisconnect(clearPromptComposer(Runtime, logger));
|
|
463
533
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
throw error;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
534
|
+
},
|
|
535
|
+
logger,
|
|
536
|
+
});
|
|
537
|
+
baselineTurns = submission.baselineTurns;
|
|
538
|
+
baselineAssistantText = submission.baselineAssistantText;
|
|
472
539
|
}
|
|
473
540
|
finally {
|
|
474
541
|
await releaseProfileLockIfHeld();
|
|
475
542
|
}
|
|
476
543
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
477
544
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
478
|
-
const normalizeForComparison = (text) => text.toLowerCase().replace(/\s+/g,
|
|
545
|
+
const normalizeForComparison = (text) => text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
546
|
+
const expectedConversationId = () => lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined;
|
|
479
547
|
const waitForFreshAssistantResponse = async (baselineNormalized, timeoutMs) => {
|
|
480
548
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
481
549
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
482
|
-
:
|
|
550
|
+
: "";
|
|
483
551
|
const deadline = Date.now() + timeoutMs;
|
|
484
552
|
while (Date.now() < deadline) {
|
|
485
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
486
|
-
const text = typeof snapshot?.text ===
|
|
553
|
+
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
554
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
487
555
|
if (text) {
|
|
488
556
|
const normalized = normalizeForComparison(text);
|
|
489
|
-
const isBaseline = normalized === baselineNormalized ||
|
|
557
|
+
const isBaseline = normalized === baselineNormalized ||
|
|
558
|
+
(baselinePrefix.length > 0 && normalized.startsWith(baselinePrefix));
|
|
490
559
|
if (!isBaseline) {
|
|
491
560
|
return {
|
|
492
561
|
text,
|
|
493
562
|
html: snapshot?.html ?? undefined,
|
|
494
|
-
meta: {
|
|
563
|
+
meta: {
|
|
564
|
+
turnId: snapshot?.turnId ?? undefined,
|
|
565
|
+
messageId: snapshot?.messageId ?? undefined,
|
|
566
|
+
},
|
|
495
567
|
};
|
|
496
568
|
}
|
|
497
569
|
}
|
|
@@ -507,7 +579,7 @@ export async function runBrowserMode(options) {
|
|
|
507
579
|
return null;
|
|
508
580
|
logger(`[browser] Assistant response timed out; waiting ${formatElapsed(recheckDelayMs)} before rechecking conversation.`);
|
|
509
581
|
await raceWithDisconnect(delay(recheckDelayMs));
|
|
510
|
-
await updateConversationHint(
|
|
582
|
+
await updateConversationHint("assistant-recheck", 15_000).catch(() => false);
|
|
511
583
|
await captureRuntimeSnapshot().catch(() => undefined);
|
|
512
584
|
const conversationUrl = await readConversationUrl(Runtime);
|
|
513
585
|
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
@@ -522,12 +594,12 @@ export async function runBrowserMode(options) {
|
|
|
522
594
|
// Update session metadata to indicate login is needed
|
|
523
595
|
await emitRuntimeHint();
|
|
524
596
|
throw new BrowserAutomationError(`ChatGPT session expired during recheck: ${sessionValid.reason}. ` +
|
|
525
|
-
`Conversation URL: ${conversationUrl || lastUrl ||
|
|
597
|
+
`Conversation URL: ${conversationUrl || lastUrl || "unknown"}. ` +
|
|
526
598
|
`Please sign in and retry.`, {
|
|
527
|
-
stage:
|
|
599
|
+
stage: "assistant-recheck",
|
|
528
600
|
details: {
|
|
529
601
|
conversationUrl: conversationUrl || lastUrl || null,
|
|
530
|
-
sessionStatus:
|
|
602
|
+
sessionStatus: "needs_login",
|
|
531
603
|
validationReason: sessionValid.reason,
|
|
532
604
|
},
|
|
533
605
|
runtime: {
|
|
@@ -543,12 +615,13 @@ export async function runBrowserMode(options) {
|
|
|
543
615
|
});
|
|
544
616
|
}
|
|
545
617
|
const timeoutMs = recheckTimeoutMs > 0 ? recheckTimeoutMs : config.timeoutMs;
|
|
546
|
-
const rechecked = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined));
|
|
547
|
-
logger(
|
|
618
|
+
const rechecked = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId()));
|
|
619
|
+
logger("Recovered assistant response after delayed recheck");
|
|
548
620
|
return rechecked;
|
|
549
621
|
};
|
|
550
622
|
try {
|
|
551
|
-
|
|
623
|
+
await updateConversationHint("assistant-wait", 15_000).catch(() => false);
|
|
624
|
+
answer = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId()));
|
|
552
625
|
}
|
|
553
626
|
catch (error) {
|
|
554
627
|
if (isAssistantResponseTimeoutError(error)) {
|
|
@@ -557,7 +630,7 @@ export async function runBrowserMode(options) {
|
|
|
557
630
|
answer = rechecked;
|
|
558
631
|
}
|
|
559
632
|
else {
|
|
560
|
-
await updateConversationHint(
|
|
633
|
+
await updateConversationHint("assistant-timeout", 15_000).catch(() => false);
|
|
561
634
|
await captureRuntimeSnapshot().catch(() => undefined);
|
|
562
635
|
const runtime = {
|
|
563
636
|
chromePid: chrome.pid,
|
|
@@ -569,7 +642,7 @@ export async function runBrowserMode(options) {
|
|
|
569
642
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
570
643
|
controllerPid: process.pid,
|
|
571
644
|
};
|
|
572
|
-
throw new BrowserAutomationError(
|
|
645
|
+
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime }, error);
|
|
573
646
|
}
|
|
574
647
|
}
|
|
575
648
|
else {
|
|
@@ -577,17 +650,19 @@ export async function runBrowserMode(options) {
|
|
|
577
650
|
}
|
|
578
651
|
}
|
|
579
652
|
// Ensure we store the final conversation URL even if the UI updated late.
|
|
580
|
-
await updateConversationHint(
|
|
581
|
-
const baselineNormalized = baselineAssistantText
|
|
653
|
+
await updateConversationHint("post-response", 15_000);
|
|
654
|
+
const baselineNormalized = baselineAssistantText
|
|
655
|
+
? normalizeForComparison(baselineAssistantText)
|
|
656
|
+
: "";
|
|
582
657
|
if (baselineNormalized) {
|
|
583
|
-
const normalizedAnswer = normalizeForComparison(answer.text ??
|
|
658
|
+
const normalizedAnswer = normalizeForComparison(answer.text ?? "");
|
|
584
659
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
585
660
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
586
|
-
:
|
|
661
|
+
: "";
|
|
587
662
|
const isBaseline = normalizedAnswer === baselineNormalized ||
|
|
588
663
|
(baselinePrefix.length > 0 && normalizedAnswer.startsWith(baselinePrefix));
|
|
589
664
|
if (isBaseline) {
|
|
590
|
-
logger(
|
|
665
|
+
logger("Detected stale assistant response; waiting for new response...");
|
|
591
666
|
const refreshed = await waitForFreshAssistantResponse(baselineNormalized, 15_000);
|
|
592
667
|
if (refreshed) {
|
|
593
668
|
answer = refreshed;
|
|
@@ -595,11 +670,11 @@ export async function runBrowserMode(options) {
|
|
|
595
670
|
}
|
|
596
671
|
}
|
|
597
672
|
answerText = answer.text;
|
|
598
|
-
answerHtml = answer.html ??
|
|
673
|
+
answerHtml = answer.html ?? "";
|
|
599
674
|
const copiedMarkdown = await raceWithDisconnect(withRetries(async () => {
|
|
600
675
|
const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
|
|
601
676
|
if (!attempt) {
|
|
602
|
-
throw new Error(
|
|
677
|
+
throw new Error("copy-missing");
|
|
603
678
|
}
|
|
604
679
|
return attempt;
|
|
605
680
|
}, {
|
|
@@ -622,8 +697,8 @@ export async function runBrowserMode(options) {
|
|
|
622
697
|
allowMarkdownUpdate: !copiedMarkdown,
|
|
623
698
|
}));
|
|
624
699
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
625
|
-
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
626
|
-
const finalText = typeof finalSnapshot?.text ===
|
|
700
|
+
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
701
|
+
const finalText = typeof finalSnapshot?.text === "string" ? finalSnapshot.text.trim() : "";
|
|
627
702
|
if (finalText && finalText !== promptText.trim()) {
|
|
628
703
|
const trimmedMarkdown = answerMarkdown.trim();
|
|
629
704
|
const finalIsEcho = promptEchoMatcher ? promptEchoMatcher.isEcho(finalText) : false;
|
|
@@ -633,27 +708,27 @@ export async function runBrowserMode(options) {
|
|
|
633
708
|
trimmedMarkdown.length > 0 &&
|
|
634
709
|
lengthDelta >= Math.max(12, Math.floor(trimmedMarkdown.length * 0.75));
|
|
635
710
|
if ((missingCopy || likelyTruncatedCopy) && !finalIsEcho && finalText !== trimmedMarkdown) {
|
|
636
|
-
logger(
|
|
711
|
+
logger("Refreshed assistant response via final DOM snapshot");
|
|
637
712
|
answerText = finalText;
|
|
638
713
|
answerMarkdown = finalText;
|
|
639
714
|
}
|
|
640
715
|
}
|
|
641
716
|
// Detect prompt echo using normalized comparison (whitespace-insensitive).
|
|
642
717
|
const alignedEcho = alignPromptEchoPair(answerText, answerMarkdown, promptEchoMatcher, copiedMarkdown ? logger : undefined, {
|
|
643
|
-
text:
|
|
644
|
-
markdown:
|
|
718
|
+
text: "Aligned assistant response text to copied markdown after prompt echo",
|
|
719
|
+
markdown: "Aligned assistant markdown to response text after prompt echo",
|
|
645
720
|
});
|
|
646
721
|
answerText = alignedEcho.answerText;
|
|
647
722
|
answerMarkdown = alignedEcho.answerMarkdown;
|
|
648
723
|
const isPromptEcho = alignedEcho.isEcho;
|
|
649
724
|
if (isPromptEcho) {
|
|
650
|
-
logger(
|
|
725
|
+
logger("Detected prompt echo in response; waiting for actual assistant response...");
|
|
651
726
|
const deadline = Date.now() + 15_000;
|
|
652
727
|
let bestText = null;
|
|
653
728
|
let stableCount = 0;
|
|
654
729
|
while (Date.now() < deadline) {
|
|
655
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
656
|
-
const text = typeof snapshot?.text ===
|
|
730
|
+
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
731
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
657
732
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
658
733
|
if (!isStillEcho) {
|
|
659
734
|
if (!bestText || text.length > bestText.length) {
|
|
@@ -670,7 +745,7 @@ export async function runBrowserMode(options) {
|
|
|
670
745
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
671
746
|
}
|
|
672
747
|
if (bestText) {
|
|
673
|
-
logger(
|
|
748
|
+
logger("Recovered assistant response after detecting prompt echo");
|
|
674
749
|
answerText = bestText;
|
|
675
750
|
answerMarkdown = bestText;
|
|
676
751
|
}
|
|
@@ -681,8 +756,8 @@ export async function runBrowserMode(options) {
|
|
|
681
756
|
let bestText = answerText.trim();
|
|
682
757
|
let stableCycles = 0;
|
|
683
758
|
while (Date.now() < deadline) {
|
|
684
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
685
|
-
const text = typeof snapshot?.text ===
|
|
759
|
+
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
760
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
686
761
|
if (text && text.length > bestText.length) {
|
|
687
762
|
bestText = text;
|
|
688
763
|
stableCycles = 0;
|
|
@@ -696,17 +771,17 @@ export async function runBrowserMode(options) {
|
|
|
696
771
|
await delay(400);
|
|
697
772
|
}
|
|
698
773
|
if (bestText.length > answerText.trim().length) {
|
|
699
|
-
logger(
|
|
774
|
+
logger("Refreshed short assistant response from latest DOM snapshot");
|
|
700
775
|
answerText = bestText;
|
|
701
776
|
answerMarkdown = bestText;
|
|
702
777
|
}
|
|
703
778
|
}
|
|
704
779
|
if (connectionClosedUnexpectedly) {
|
|
705
780
|
// Bail out on mid-run disconnects so the session stays reattachable.
|
|
706
|
-
throw new Error(
|
|
781
|
+
throw new Error("Chrome disconnected before completion");
|
|
707
782
|
}
|
|
708
783
|
stopThinkingMonitor?.();
|
|
709
|
-
runStatus =
|
|
784
|
+
runStatus = "complete";
|
|
710
785
|
const durationMs = Date.now() - startedAt;
|
|
711
786
|
const answerChars = answerText.length;
|
|
712
787
|
const answerTokens = estimateTokenCount(answerMarkdown);
|
|
@@ -731,20 +806,42 @@ export async function runBrowserMode(options) {
|
|
|
731
806
|
stopThinkingMonitor?.();
|
|
732
807
|
const socketClosed = connectionClosedUnexpectedly || isWebSocketClosureError(normalizedError);
|
|
733
808
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
809
|
+
if (shouldPreserveBrowserOnError(normalizedError, config.headless)) {
|
|
810
|
+
preserveBrowserOnError = true;
|
|
811
|
+
const runtime = {
|
|
812
|
+
chromePid: chrome.pid,
|
|
813
|
+
chromePort: chrome.port,
|
|
814
|
+
chromeHost,
|
|
815
|
+
userDataDir,
|
|
816
|
+
chromeTargetId: lastTargetId,
|
|
817
|
+
tabUrl: lastUrl,
|
|
818
|
+
controllerPid: process.pid,
|
|
819
|
+
};
|
|
820
|
+
const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
|
|
821
|
+
`--browser-manual-login-profile-dir ${JSON.stringify(userDataDir)}`;
|
|
822
|
+
await emitRuntimeHint();
|
|
823
|
+
logger("Cloudflare challenge detected; leaving browser open so you can complete the check.");
|
|
824
|
+
logger(`Reuse this browser profile with: ${reuseProfileHint}`);
|
|
825
|
+
throw new BrowserAutomationError("Cloudflare challenge detected. Complete the “Just a moment…” check in the open browser, then rerun.", {
|
|
826
|
+
stage: "cloudflare-challenge",
|
|
827
|
+
runtime,
|
|
828
|
+
reuseProfileHint,
|
|
829
|
+
}, normalizedError);
|
|
830
|
+
}
|
|
734
831
|
if (!socketClosed) {
|
|
735
832
|
logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
|
|
736
|
-
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
833
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
737
834
|
logger(normalizedError.stack);
|
|
738
835
|
}
|
|
739
836
|
throw normalizedError;
|
|
740
837
|
}
|
|
741
|
-
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
838
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
742
839
|
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
743
840
|
logger(normalizedError.stack);
|
|
744
841
|
}
|
|
745
842
|
await emitRuntimeHint();
|
|
746
|
-
throw new BrowserAutomationError(
|
|
747
|
-
stage:
|
|
843
|
+
throw new BrowserAutomationError("Chrome window closed before oracle finished. Please keep it open until completion.", {
|
|
844
|
+
stage: "connection-lost",
|
|
748
845
|
runtime: {
|
|
749
846
|
chromePid: chrome.pid,
|
|
750
847
|
chromePort: chrome.port,
|
|
@@ -768,12 +865,13 @@ export async function runBrowserMode(options) {
|
|
|
768
865
|
// Close the isolated tab once the response has been fully captured to prevent
|
|
769
866
|
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
770
867
|
// so reattach can recover the response.
|
|
771
|
-
if (runStatus ===
|
|
868
|
+
if (runStatus === "complete" && isolatedTargetId && chrome?.port) {
|
|
772
869
|
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
773
870
|
}
|
|
774
871
|
removeDialogHandler?.();
|
|
775
872
|
removeTerminationHooks?.();
|
|
776
|
-
|
|
873
|
+
const keepBrowserOpen = effectiveKeepBrowser || preserveBrowserOnError;
|
|
874
|
+
if (!keepBrowserOpen) {
|
|
777
875
|
if (!connectionClosedUnexpectedly) {
|
|
778
876
|
try {
|
|
779
877
|
await chrome.kill();
|
|
@@ -789,7 +887,7 @@ export async function runBrowserMode(options) {
|
|
|
789
887
|
});
|
|
790
888
|
if (shouldCleanup) {
|
|
791
889
|
// Preserve the persistent manual-login profile, but clear stale reattach hints.
|
|
792
|
-
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode:
|
|
890
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
793
891
|
}
|
|
794
892
|
}
|
|
795
893
|
else {
|
|
@@ -821,28 +919,28 @@ async function pickAvailableDebugPort(preferredPort, logger) {
|
|
|
821
919
|
async function isPortAvailable(port) {
|
|
822
920
|
return new Promise((resolve) => {
|
|
823
921
|
const server = net.createServer();
|
|
824
|
-
server.once(
|
|
825
|
-
server.once(
|
|
922
|
+
server.once("error", () => resolve(false));
|
|
923
|
+
server.once("listening", () => {
|
|
826
924
|
server.close(() => resolve(true));
|
|
827
925
|
});
|
|
828
|
-
server.listen(port,
|
|
926
|
+
server.listen(port, "127.0.0.1");
|
|
829
927
|
});
|
|
830
928
|
}
|
|
831
929
|
async function findEphemeralPort() {
|
|
832
930
|
return new Promise((resolve, reject) => {
|
|
833
931
|
const server = net.createServer();
|
|
834
|
-
server.once(
|
|
932
|
+
server.once("error", (error) => {
|
|
835
933
|
server.close();
|
|
836
934
|
reject(error);
|
|
837
935
|
});
|
|
838
|
-
server.listen(0,
|
|
936
|
+
server.listen(0, "127.0.0.1", () => {
|
|
839
937
|
const address = server.address();
|
|
840
|
-
if (address && typeof address ===
|
|
938
|
+
if (address && typeof address === "object") {
|
|
841
939
|
const port = address.port;
|
|
842
940
|
server.close(() => resolve(port));
|
|
843
941
|
}
|
|
844
942
|
else {
|
|
845
|
-
server.close(() => reject(new Error(
|
|
943
|
+
server.close(() => reject(new Error("Failed to acquire ephemeral port")));
|
|
846
944
|
}
|
|
847
945
|
});
|
|
848
946
|
});
|
|
@@ -861,20 +959,20 @@ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, time
|
|
|
861
959
|
}
|
|
862
960
|
catch (error) {
|
|
863
961
|
const message = error instanceof Error ? error.message : String(error);
|
|
864
|
-
const loginDetected = message?.toLowerCase().includes(
|
|
865
|
-
const sessionMissing = message?.toLowerCase().includes(
|
|
962
|
+
const loginDetected = message?.toLowerCase().includes("login button");
|
|
963
|
+
const sessionMissing = message?.toLowerCase().includes("session not detected");
|
|
866
964
|
if (!loginDetected && !sessionMissing) {
|
|
867
965
|
throw error;
|
|
868
966
|
}
|
|
869
967
|
const now = Date.now();
|
|
870
968
|
if (now - lastNotice > 5000) {
|
|
871
|
-
logger(
|
|
969
|
+
logger("Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...");
|
|
872
970
|
lastNotice = now;
|
|
873
971
|
}
|
|
874
972
|
await delay(1000);
|
|
875
973
|
}
|
|
876
974
|
}
|
|
877
|
-
throw new Error(
|
|
975
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
|
|
878
976
|
}
|
|
879
977
|
async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answerText, answerMarkdown, logger, allowMarkdownUpdate, }) {
|
|
880
978
|
// Learned: long streaming responses can still be rendering after initial capture.
|
|
@@ -888,7 +986,7 @@ async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answe
|
|
|
888
986
|
let bestText = answerText;
|
|
889
987
|
for (let i = 0; i < 5; i++) {
|
|
890
988
|
const laterSnapshot = await readAssistantSnapshot(runtime, baselineTurns ?? undefined).catch(() => null);
|
|
891
|
-
const laterText = typeof laterSnapshot?.text ===
|
|
989
|
+
const laterText = typeof laterSnapshot?.text === "string" ? laterSnapshot.text.trim() : "";
|
|
892
990
|
if (laterText.length > bestLength) {
|
|
893
991
|
bestLength = laterText.length;
|
|
894
992
|
bestText = laterText;
|
|
@@ -909,22 +1007,22 @@ async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answe
|
|
|
909
1007
|
}
|
|
910
1008
|
async function _assertNavigatedToHttp(runtime, _logger, timeoutMs = 10_000) {
|
|
911
1009
|
const deadline = Date.now() + timeoutMs;
|
|
912
|
-
let lastUrl =
|
|
1010
|
+
let lastUrl = "";
|
|
913
1011
|
while (Date.now() < deadline) {
|
|
914
1012
|
const { result } = await runtime.evaluate({
|
|
915
1013
|
expression: 'typeof location === "object" && location.href ? location.href : ""',
|
|
916
1014
|
returnByValue: true,
|
|
917
1015
|
});
|
|
918
|
-
const url = typeof result?.value ===
|
|
1016
|
+
const url = typeof result?.value === "string" ? result.value : "";
|
|
919
1017
|
lastUrl = url;
|
|
920
1018
|
if (/^https?:\/\//i.test(url)) {
|
|
921
1019
|
return url;
|
|
922
1020
|
}
|
|
923
1021
|
await delay(250);
|
|
924
1022
|
}
|
|
925
|
-
throw new BrowserAutomationError(
|
|
926
|
-
stage:
|
|
927
|
-
details: { url: lastUrl ||
|
|
1023
|
+
throw new BrowserAutomationError("ChatGPT session not detected; page never left new tab.", {
|
|
1024
|
+
stage: "execute-browser",
|
|
1025
|
+
details: { url: lastUrl || "(empty)" },
|
|
928
1026
|
});
|
|
929
1027
|
}
|
|
930
1028
|
async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
@@ -944,11 +1042,11 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
|
944
1042
|
if (!probe.ok) {
|
|
945
1043
|
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${probe.error}); launching new Chrome.`);
|
|
946
1044
|
// Safe cleanup: remove stale DevToolsActivePort; only remove lock files if this was an Oracle-owned pid that died.
|
|
947
|
-
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode:
|
|
1045
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
|
|
948
1046
|
return null;
|
|
949
1047
|
}
|
|
950
1048
|
const pid = await readChromePid(userDataDir);
|
|
951
|
-
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` :
|
|
1049
|
+
logger(`Found running Chrome for ${userDataDir}; reusing (DevTools port ${port}${pid ? `, pid ${pid}` : ""})`);
|
|
952
1050
|
return {
|
|
953
1051
|
port,
|
|
954
1052
|
pid: pid ?? undefined,
|
|
@@ -959,7 +1057,7 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
|
959
1057
|
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
960
1058
|
const remoteChromeConfig = config.remoteChrome;
|
|
961
1059
|
if (!remoteChromeConfig) {
|
|
962
|
-
throw new Error(
|
|
1060
|
+
throw new Error("Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.");
|
|
963
1061
|
}
|
|
964
1062
|
const { host, port } = remoteChromeConfig;
|
|
965
1063
|
logger(`Connecting to remote Chrome at ${host}:${port}`);
|
|
@@ -985,9 +1083,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
985
1083
|
}
|
|
986
1084
|
};
|
|
987
1085
|
const startedAt = Date.now();
|
|
988
|
-
let answerText =
|
|
989
|
-
let answerMarkdown =
|
|
990
|
-
let answerHtml =
|
|
1086
|
+
let answerText = "";
|
|
1087
|
+
let answerMarkdown = "";
|
|
1088
|
+
let answerHtml = "";
|
|
991
1089
|
let connectionClosedUnexpectedly = false;
|
|
992
1090
|
let stopThinkingMonitor = null;
|
|
993
1091
|
let removeDialogHandler = null;
|
|
@@ -999,16 +1097,16 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
999
1097
|
const markConnectionLost = () => {
|
|
1000
1098
|
connectionClosedUnexpectedly = true;
|
|
1001
1099
|
};
|
|
1002
|
-
client.on(
|
|
1100
|
+
client.on("disconnect", markConnectionLost);
|
|
1003
1101
|
const { Network, Page, Runtime, Input, DOM } = client;
|
|
1004
1102
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
1005
|
-
if (DOM && typeof DOM.enable ===
|
|
1103
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
1006
1104
|
domainEnablers.push(DOM.enable());
|
|
1007
1105
|
}
|
|
1008
1106
|
await Promise.all(domainEnablers);
|
|
1009
1107
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
1010
1108
|
// Skip cookie sync for remote Chrome - it already has cookies
|
|
1011
|
-
logger(
|
|
1109
|
+
logger("Skipping cookie sync for remote Chrome (using existing session)");
|
|
1012
1110
|
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
1013
1111
|
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
1014
1112
|
await ensureLoggedIn(Runtime, logger, { remoteSession: true });
|
|
@@ -1016,10 +1114,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1016
1114
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1017
1115
|
try {
|
|
1018
1116
|
const { result } = await Runtime.evaluate({
|
|
1019
|
-
expression:
|
|
1117
|
+
expression: "location.href",
|
|
1020
1118
|
returnByValue: true,
|
|
1021
1119
|
});
|
|
1022
|
-
if (typeof result?.value ===
|
|
1120
|
+
if (typeof result?.value === "string") {
|
|
1023
1121
|
lastUrl = result.value;
|
|
1024
1122
|
}
|
|
1025
1123
|
await emitRuntimeHint();
|
|
@@ -1028,7 +1126,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1028
1126
|
// ignore
|
|
1029
1127
|
}
|
|
1030
1128
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
1031
|
-
if (config.desiredModel && modelStrategy !==
|
|
1129
|
+
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
1032
1130
|
await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
1033
1131
|
retries: 2,
|
|
1034
1132
|
delayMs: 300,
|
|
@@ -1041,8 +1139,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1041
1139
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
1042
1140
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1043
1141
|
}
|
|
1044
|
-
else if (modelStrategy ===
|
|
1045
|
-
logger(
|
|
1142
|
+
else if (modelStrategy === "ignore") {
|
|
1143
|
+
logger("Model picker: skipped (strategy=ignore)");
|
|
1046
1144
|
}
|
|
1047
1145
|
// Handle thinking time selection if specified
|
|
1048
1146
|
const thinkingTime = config.thinkingTime;
|
|
@@ -1059,11 +1157,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1059
1157
|
}
|
|
1060
1158
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
1061
1159
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
1062
|
-
const baselineAssistantText = typeof baselineSnapshot?.text ===
|
|
1160
|
+
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
1063
1161
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
1064
1162
|
if (submissionAttachments.length > 0) {
|
|
1065
1163
|
if (!DOM) {
|
|
1066
|
-
throw new Error(
|
|
1164
|
+
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
1067
1165
|
}
|
|
1068
1166
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
1069
1167
|
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
@@ -1077,64 +1175,76 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1077
1175
|
const perFileTimeout = 15_000;
|
|
1078
1176
|
const waitBudget = Math.max(baseTimeout, 30_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
1079
1177
|
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
1080
|
-
logger(
|
|
1178
|
+
logger("All attachments uploaded");
|
|
1081
1179
|
}
|
|
1082
1180
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
1083
|
-
const
|
|
1181
|
+
const providerState = {
|
|
1084
1182
|
runtime: Runtime,
|
|
1085
1183
|
input: Input,
|
|
1086
|
-
|
|
1087
|
-
|
|
1184
|
+
logger,
|
|
1185
|
+
timeoutMs: config.timeoutMs,
|
|
1088
1186
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1187
|
+
baselineTurns: baselineTurns ?? undefined,
|
|
1188
|
+
attachmentNames,
|
|
1189
|
+
};
|
|
1190
|
+
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
1191
|
+
prompt,
|
|
1192
|
+
evaluate: async () => undefined,
|
|
1193
|
+
delay,
|
|
1194
|
+
log: logger,
|
|
1195
|
+
state: providerState,
|
|
1196
|
+
});
|
|
1197
|
+
const providerBaselineTurns = providerState.baselineTurns;
|
|
1198
|
+
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
1199
|
+
baselineTurns = providerBaselineTurns;
|
|
1094
1200
|
}
|
|
1095
1201
|
return { baselineTurns, baselineAssistantText };
|
|
1096
1202
|
};
|
|
1203
|
+
const reloadPromptComposer = async () => {
|
|
1204
|
+
logger("[browser] Composer became unresponsive; reloading page and retrying once.");
|
|
1205
|
+
await Page.reload({ ignoreCache: true });
|
|
1206
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
1207
|
+
};
|
|
1097
1208
|
let baselineTurns = null;
|
|
1098
1209
|
let baselineAssistantText = null;
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
error.details?.code === 'prompt-too-large';
|
|
1107
|
-
if (options.fallbackSubmission && isPromptTooLarge) {
|
|
1108
|
-
logger('[browser] Inline prompt too large; retrying with file uploads.');
|
|
1210
|
+
const submission = await runSubmissionWithRecovery({
|
|
1211
|
+
prompt: promptText,
|
|
1212
|
+
attachments,
|
|
1213
|
+
fallbackSubmission: options.fallbackSubmission,
|
|
1214
|
+
submit: submitOnce,
|
|
1215
|
+
reloadPromptComposer,
|
|
1216
|
+
prepareFallbackSubmission: async () => {
|
|
1109
1217
|
await clearPromptComposer(Runtime, logger);
|
|
1110
1218
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
throw error;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1219
|
+
},
|
|
1220
|
+
logger,
|
|
1221
|
+
});
|
|
1222
|
+
baselineTurns = submission.baselineTurns;
|
|
1223
|
+
baselineAssistantText = submission.baselineAssistantText;
|
|
1119
1224
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
1120
1225
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
1121
|
-
const normalizeForComparison = (text) => text.toLowerCase().replace(/\s+/g,
|
|
1226
|
+
const normalizeForComparison = (text) => text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
1227
|
+
const expectedConversationId = () => lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined;
|
|
1122
1228
|
const waitForFreshAssistantResponse = async (baselineNormalized, timeoutMs) => {
|
|
1123
1229
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
1124
1230
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
1125
|
-
:
|
|
1231
|
+
: "";
|
|
1126
1232
|
const deadline = Date.now() + timeoutMs;
|
|
1127
1233
|
while (Date.now() < deadline) {
|
|
1128
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1129
|
-
const text = typeof snapshot?.text ===
|
|
1234
|
+
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1235
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1130
1236
|
if (text) {
|
|
1131
1237
|
const normalized = normalizeForComparison(text);
|
|
1132
|
-
const isBaseline = normalized === baselineNormalized ||
|
|
1238
|
+
const isBaseline = normalized === baselineNormalized ||
|
|
1239
|
+
(baselinePrefix.length > 0 && normalized.startsWith(baselinePrefix));
|
|
1133
1240
|
if (!isBaseline) {
|
|
1134
1241
|
return {
|
|
1135
1242
|
text,
|
|
1136
1243
|
html: snapshot?.html ?? undefined,
|
|
1137
|
-
meta: {
|
|
1244
|
+
meta: {
|
|
1245
|
+
turnId: snapshot?.turnId ?? undefined,
|
|
1246
|
+
messageId: snapshot?.messageId ?? undefined,
|
|
1247
|
+
},
|
|
1138
1248
|
};
|
|
1139
1249
|
}
|
|
1140
1250
|
}
|
|
@@ -1164,12 +1274,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1164
1274
|
// Update session metadata to indicate login is needed
|
|
1165
1275
|
await emitRuntimeHint();
|
|
1166
1276
|
throw new BrowserAutomationError(`ChatGPT session expired during recheck: ${sessionValid.reason}. ` +
|
|
1167
|
-
`Conversation URL: ${conversationUrl || lastUrl ||
|
|
1277
|
+
`Conversation URL: ${conversationUrl || lastUrl || "unknown"}. ` +
|
|
1168
1278
|
`Please sign in and retry.`, {
|
|
1169
|
-
stage:
|
|
1279
|
+
stage: "assistant-recheck",
|
|
1170
1280
|
details: {
|
|
1171
1281
|
conversationUrl: conversationUrl || lastUrl || null,
|
|
1172
|
-
sessionStatus:
|
|
1282
|
+
sessionStatus: "needs_login",
|
|
1173
1283
|
validationReason: sessionValid.reason,
|
|
1174
1284
|
},
|
|
1175
1285
|
runtime: {
|
|
@@ -1184,12 +1294,17 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1184
1294
|
}
|
|
1185
1295
|
await emitRuntimeHint();
|
|
1186
1296
|
const timeoutMs = recheckTimeoutMs > 0 ? recheckTimeoutMs : config.timeoutMs;
|
|
1187
|
-
const rechecked = await waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined);
|
|
1188
|
-
logger(
|
|
1297
|
+
const rechecked = await waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId());
|
|
1298
|
+
logger("Recovered assistant response after delayed recheck");
|
|
1189
1299
|
return rechecked;
|
|
1190
1300
|
};
|
|
1191
1301
|
try {
|
|
1192
|
-
|
|
1302
|
+
const conversationUrl = await readConversationUrl(Runtime).catch(() => null);
|
|
1303
|
+
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
1304
|
+
lastUrl = conversationUrl;
|
|
1305
|
+
await emitRuntimeHint();
|
|
1306
|
+
}
|
|
1307
|
+
answer = await waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId());
|
|
1193
1308
|
}
|
|
1194
1309
|
catch (error) {
|
|
1195
1310
|
if (isAssistantResponseTimeoutError(error)) {
|
|
@@ -1216,23 +1331,25 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1216
1331
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1217
1332
|
controllerPid: process.pid,
|
|
1218
1333
|
};
|
|
1219
|
-
throw new BrowserAutomationError(
|
|
1334
|
+
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime }, error);
|
|
1220
1335
|
}
|
|
1221
1336
|
}
|
|
1222
1337
|
else {
|
|
1223
1338
|
throw error;
|
|
1224
1339
|
}
|
|
1225
1340
|
}
|
|
1226
|
-
const baselineNormalized = baselineAssistantText
|
|
1341
|
+
const baselineNormalized = baselineAssistantText
|
|
1342
|
+
? normalizeForComparison(baselineAssistantText)
|
|
1343
|
+
: "";
|
|
1227
1344
|
if (baselineNormalized) {
|
|
1228
|
-
const normalizedAnswer = normalizeForComparison(answer.text ??
|
|
1345
|
+
const normalizedAnswer = normalizeForComparison(answer.text ?? "");
|
|
1229
1346
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
1230
1347
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
1231
|
-
:
|
|
1348
|
+
: "";
|
|
1232
1349
|
const isBaseline = normalizedAnswer === baselineNormalized ||
|
|
1233
1350
|
(baselinePrefix.length > 0 && normalizedAnswer.startsWith(baselinePrefix));
|
|
1234
1351
|
if (isBaseline) {
|
|
1235
|
-
logger(
|
|
1352
|
+
logger("Detected stale assistant response; waiting for new response...");
|
|
1236
1353
|
const refreshed = await waitForFreshAssistantResponse(baselineNormalized, 15_000);
|
|
1237
1354
|
if (refreshed) {
|
|
1238
1355
|
answer = refreshed;
|
|
@@ -1240,11 +1357,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1240
1357
|
}
|
|
1241
1358
|
}
|
|
1242
1359
|
answerText = answer.text;
|
|
1243
|
-
answerHtml = answer.html ??
|
|
1360
|
+
answerHtml = answer.html ?? "";
|
|
1244
1361
|
const copiedMarkdown = await withRetries(async () => {
|
|
1245
1362
|
const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
|
|
1246
1363
|
if (!attempt) {
|
|
1247
|
-
throw new Error(
|
|
1364
|
+
throw new Error("copy-missing");
|
|
1248
1365
|
}
|
|
1249
1366
|
return attempt;
|
|
1250
1367
|
}, {
|
|
@@ -1266,33 +1383,33 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1266
1383
|
allowMarkdownUpdate: !copiedMarkdown,
|
|
1267
1384
|
}));
|
|
1268
1385
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
1269
|
-
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1270
|
-
const finalText = typeof finalSnapshot?.text ===
|
|
1386
|
+
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1387
|
+
const finalText = typeof finalSnapshot?.text === "string" ? finalSnapshot.text.trim() : "";
|
|
1271
1388
|
if (finalText &&
|
|
1272
1389
|
finalText !== answerMarkdown.trim() &&
|
|
1273
1390
|
finalText !== promptText.trim() &&
|
|
1274
1391
|
finalText.length >= answerMarkdown.trim().length) {
|
|
1275
|
-
logger(
|
|
1392
|
+
logger("Refreshed assistant response via final DOM snapshot");
|
|
1276
1393
|
answerText = finalText;
|
|
1277
1394
|
answerMarkdown = finalText;
|
|
1278
1395
|
}
|
|
1279
1396
|
// Detect prompt echo using normalized comparison (whitespace-insensitive).
|
|
1280
1397
|
const promptEchoMatcher = buildPromptEchoMatcher(promptText);
|
|
1281
1398
|
const alignedEcho = alignPromptEchoPair(answerText, answerMarkdown, promptEchoMatcher, copiedMarkdown ? logger : undefined, {
|
|
1282
|
-
text:
|
|
1283
|
-
markdown:
|
|
1399
|
+
text: "Aligned assistant response text to copied markdown after prompt echo",
|
|
1400
|
+
markdown: "Aligned assistant markdown to response text after prompt echo",
|
|
1284
1401
|
});
|
|
1285
1402
|
answerText = alignedEcho.answerText;
|
|
1286
1403
|
answerMarkdown = alignedEcho.answerMarkdown;
|
|
1287
1404
|
const isPromptEcho = alignedEcho.isEcho;
|
|
1288
1405
|
if (isPromptEcho) {
|
|
1289
|
-
logger(
|
|
1406
|
+
logger("Detected prompt echo in response; waiting for actual assistant response...");
|
|
1290
1407
|
const deadline = Date.now() + 15_000;
|
|
1291
1408
|
let bestText = null;
|
|
1292
1409
|
let stableCount = 0;
|
|
1293
1410
|
while (Date.now() < deadline) {
|
|
1294
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1295
|
-
const text = typeof snapshot?.text ===
|
|
1411
|
+
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined, expectedConversationId()).catch(() => null);
|
|
1412
|
+
const text = typeof snapshot?.text === "string" ? snapshot.text.trim() : "";
|
|
1296
1413
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
1297
1414
|
if (!isStillEcho) {
|
|
1298
1415
|
if (!bestText || text.length > bestText.length) {
|
|
@@ -1309,7 +1426,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1309
1426
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1310
1427
|
}
|
|
1311
1428
|
if (bestText) {
|
|
1312
|
-
logger(
|
|
1429
|
+
logger("Recovered assistant response after detecting prompt echo");
|
|
1313
1430
|
answerText = bestText;
|
|
1314
1431
|
answerMarkdown = bestText;
|
|
1315
1432
|
}
|
|
@@ -1341,13 +1458,13 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1341
1458
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
1342
1459
|
if (!socketClosed) {
|
|
1343
1460
|
logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
|
|
1344
|
-
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
1461
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
1345
1462
|
logger(normalizedError.stack);
|
|
1346
1463
|
}
|
|
1347
1464
|
throw normalizedError;
|
|
1348
1465
|
}
|
|
1349
|
-
throw new BrowserAutomationError(
|
|
1350
|
-
stage:
|
|
1466
|
+
throw new BrowserAutomationError("Remote Chrome connection lost before Oracle finished.", {
|
|
1467
|
+
stage: "connection-lost",
|
|
1351
1468
|
runtime: {
|
|
1352
1469
|
chromeHost: host,
|
|
1353
1470
|
chromePort: port,
|
|
@@ -1373,19 +1490,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1373
1490
|
logger(`Remote session complete • ${totalSeconds.toFixed(1)}s total`);
|
|
1374
1491
|
}
|
|
1375
1492
|
}
|
|
1376
|
-
export { estimateTokenCount } from
|
|
1377
|
-
export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from
|
|
1378
|
-
export { syncCookies } from
|
|
1379
|
-
export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from
|
|
1493
|
+
export { estimateTokenCount } from "./utils.js";
|
|
1494
|
+
export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from "./config.js";
|
|
1495
|
+
export { syncCookies } from "./cookies.js";
|
|
1496
|
+
export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from "./pageActions.js";
|
|
1380
1497
|
export async function maybeReuseRunningChromeForTest(userDataDir, logger, options = {}) {
|
|
1381
1498
|
return maybeReuseRunningChrome(userDataDir, logger, options);
|
|
1382
1499
|
}
|
|
1383
|
-
function isWebSocketClosureError(error) {
|
|
1500
|
+
export function isWebSocketClosureError(error) {
|
|
1384
1501
|
const message = error.message.toLowerCase();
|
|
1385
|
-
return (message.includes(
|
|
1386
|
-
message.includes(
|
|
1387
|
-
message.includes(
|
|
1388
|
-
message.includes(
|
|
1502
|
+
return (message.includes("websocket connection closed") ||
|
|
1503
|
+
message.includes("websocket is closed") ||
|
|
1504
|
+
message.includes("websocket error") ||
|
|
1505
|
+
message.includes("inspected target navigated or closed") ||
|
|
1506
|
+
message.includes("target closed"));
|
|
1389
1507
|
}
|
|
1390
1508
|
export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
1391
1509
|
const elapsedMs = now - startedAt;
|
|
@@ -1393,13 +1511,13 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
|
1393
1511
|
const progress = Math.min(1, elapsedMs / 600_000); // soft target: 10 minutes
|
|
1394
1512
|
const pct = Math.round(progress * 100)
|
|
1395
1513
|
.toString()
|
|
1396
|
-
.padStart(3,
|
|
1397
|
-
const statusLabel = message ? ` — ${message}` :
|
|
1514
|
+
.padStart(3, " ");
|
|
1515
|
+
const statusLabel = message ? ` — ${message}` : "";
|
|
1398
1516
|
return `${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
1399
1517
|
}
|
|
1400
|
-
async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, minTurnIndex) {
|
|
1518
|
+
async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, minTurnIndex, expectedConversationId) {
|
|
1401
1519
|
try {
|
|
1402
|
-
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
1520
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
1403
1521
|
}
|
|
1404
1522
|
catch (error) {
|
|
1405
1523
|
if (!shouldReloadAfterAssistantError(error)) {
|
|
@@ -1409,20 +1527,20 @@ async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logg
|
|
|
1409
1527
|
if (!conversationUrl || !isConversationUrl(conversationUrl)) {
|
|
1410
1528
|
throw error;
|
|
1411
1529
|
}
|
|
1412
|
-
logger(
|
|
1530
|
+
logger("Assistant response stalled; reloading conversation and retrying once");
|
|
1413
1531
|
await Page.navigate({ url: conversationUrl });
|
|
1414
1532
|
await delay(1000);
|
|
1415
|
-
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
1533
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
1416
1534
|
}
|
|
1417
1535
|
}
|
|
1418
1536
|
function shouldReloadAfterAssistantError(error) {
|
|
1419
1537
|
if (!(error instanceof Error))
|
|
1420
1538
|
return false;
|
|
1421
1539
|
const message = error.message.toLowerCase();
|
|
1422
|
-
return (message.includes(
|
|
1423
|
-
message.includes(
|
|
1424
|
-
message.includes(
|
|
1425
|
-
message.includes(
|
|
1540
|
+
return (message.includes("assistant-response") ||
|
|
1541
|
+
message.includes("watchdog") ||
|
|
1542
|
+
message.includes("timeout") ||
|
|
1543
|
+
message.includes("capture assistant response"));
|
|
1426
1544
|
}
|
|
1427
1545
|
function isAssistantResponseTimeoutError(error) {
|
|
1428
1546
|
if (!(error instanceof Error))
|
|
@@ -1430,15 +1548,15 @@ function isAssistantResponseTimeoutError(error) {
|
|
|
1430
1548
|
const message = error.message.toLowerCase();
|
|
1431
1549
|
if (!message)
|
|
1432
1550
|
return false;
|
|
1433
|
-
return (message.includes(
|
|
1434
|
-
message.includes(
|
|
1435
|
-
message.includes(
|
|
1436
|
-
message.includes(
|
|
1551
|
+
return (message.includes("assistant-response") ||
|
|
1552
|
+
message.includes("assistant response") ||
|
|
1553
|
+
message.includes("watchdog") ||
|
|
1554
|
+
message.includes("capture assistant response"));
|
|
1437
1555
|
}
|
|
1438
1556
|
async function readConversationUrl(Runtime) {
|
|
1439
1557
|
try {
|
|
1440
|
-
const currentUrl = await Runtime.evaluate({ expression:
|
|
1441
|
-
return typeof currentUrl.result?.value ===
|
|
1558
|
+
const currentUrl = await Runtime.evaluate({ expression: "location.href", returnByValue: true });
|
|
1559
|
+
return typeof currentUrl.result?.value === "string" ? currentUrl.result.value : null;
|
|
1442
1560
|
}
|
|
1443
1561
|
catch {
|
|
1444
1562
|
return null;
|
|
@@ -1461,16 +1579,16 @@ async function validateChatGPTSession(Runtime, logger) {
|
|
|
1461
1579
|
});
|
|
1462
1580
|
const result = outcome.result?.value;
|
|
1463
1581
|
if (!result) {
|
|
1464
|
-
return { valid: false, reason:
|
|
1582
|
+
return { valid: false, reason: "Failed to evaluate session state" };
|
|
1465
1583
|
}
|
|
1466
1584
|
if (result.onAuthPage) {
|
|
1467
|
-
return { valid: false, reason:
|
|
1585
|
+
return { valid: false, reason: "Redirected to auth page" };
|
|
1468
1586
|
}
|
|
1469
1587
|
if (result.hasLoginCta) {
|
|
1470
|
-
return { valid: false, reason:
|
|
1588
|
+
return { valid: false, reason: "Login button detected on page" };
|
|
1471
1589
|
}
|
|
1472
1590
|
if (!result.hasTextarea) {
|
|
1473
|
-
return { valid: false, reason:
|
|
1591
|
+
return { valid: false, reason: "Prompt textarea not available" };
|
|
1474
1592
|
}
|
|
1475
1593
|
return { valid: true };
|
|
1476
1594
|
}
|
|
@@ -1557,9 +1675,9 @@ async function readConversationTurnCount(Runtime, logger) {
|
|
|
1557
1675
|
expression: `document.querySelectorAll(${selectorLiteral}).length`,
|
|
1558
1676
|
returnByValue: true,
|
|
1559
1677
|
});
|
|
1560
|
-
const raw = typeof result?.value ===
|
|
1678
|
+
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
1561
1679
|
if (!Number.isFinite(raw)) {
|
|
1562
|
-
throw new Error(
|
|
1680
|
+
throw new Error("Turn count not numeric");
|
|
1563
1681
|
}
|
|
1564
1682
|
return Math.max(0, Math.floor(raw));
|
|
1565
1683
|
}
|
|
@@ -1594,14 +1712,14 @@ function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false)
|
|
|
1594
1712
|
const nextMessage = await readThinkingStatus(Runtime);
|
|
1595
1713
|
if (nextMessage && nextMessage !== lastMessage) {
|
|
1596
1714
|
lastMessage = nextMessage;
|
|
1597
|
-
let locatorSuffix =
|
|
1715
|
+
let locatorSuffix = "";
|
|
1598
1716
|
if (includeDiagnostics) {
|
|
1599
1717
|
try {
|
|
1600
1718
|
const snapshot = await readAssistantSnapshot(Runtime);
|
|
1601
|
-
locatorSuffix = ` | assistant-turn=${snapshot ?
|
|
1719
|
+
locatorSuffix = ` | assistant-turn=${snapshot ? "present" : "missing"}`;
|
|
1602
1720
|
}
|
|
1603
1721
|
catch {
|
|
1604
|
-
locatorSuffix =
|
|
1722
|
+
locatorSuffix = " | assistant-turn=error";
|
|
1605
1723
|
}
|
|
1606
1724
|
}
|
|
1607
1725
|
logger(formatThinkingLog(startedAt, Date.now(), nextMessage, locatorSuffix));
|
|
@@ -1628,7 +1746,7 @@ async function readThinkingStatus(Runtime) {
|
|
|
1628
1746
|
const expression = buildThinkingStatusExpression();
|
|
1629
1747
|
try {
|
|
1630
1748
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
1631
|
-
const value = typeof result.value ===
|
|
1749
|
+
const value = typeof result.value === "string" ? result.value.trim() : "";
|
|
1632
1750
|
const sanitized = sanitizeThinkingText(value);
|
|
1633
1751
|
return sanitized || null;
|
|
1634
1752
|
}
|
|
@@ -1638,12 +1756,12 @@ async function readThinkingStatus(Runtime) {
|
|
|
1638
1756
|
}
|
|
1639
1757
|
function sanitizeThinkingText(raw) {
|
|
1640
1758
|
if (!raw) {
|
|
1641
|
-
return
|
|
1759
|
+
return "";
|
|
1642
1760
|
}
|
|
1643
1761
|
const trimmed = raw.trim();
|
|
1644
1762
|
const prefixPattern = /^(pro thinking)\s*[•:\-–—]*\s*/i;
|
|
1645
1763
|
if (prefixPattern.test(trimmed)) {
|
|
1646
|
-
return trimmed.replace(prefixPattern,
|
|
1764
|
+
return trimmed.replace(prefixPattern, "").trim();
|
|
1647
1765
|
}
|
|
1648
1766
|
return trimmed;
|
|
1649
1767
|
}
|
|
@@ -1652,20 +1770,20 @@ function describeDevtoolsFirewallHint(host, port) {
|
|
|
1652
1770
|
return null;
|
|
1653
1771
|
return [
|
|
1654
1772
|
`DevTools port ${host}:${port} is blocked from WSL.`,
|
|
1655
|
-
|
|
1656
|
-
|
|
1773
|
+
"",
|
|
1774
|
+
"PowerShell (admin):",
|
|
1657
1775
|
`New-NetFirewallRule -DisplayName 'Chrome DevTools ${port}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${port}`,
|
|
1658
1776
|
"New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
].join(
|
|
1777
|
+
"",
|
|
1778
|
+
"Re-run the same oracle command after adding the rule.",
|
|
1779
|
+
].join("\n");
|
|
1662
1780
|
}
|
|
1663
1781
|
function isWsl() {
|
|
1664
|
-
if (process.platform !==
|
|
1782
|
+
if (process.platform !== "linux")
|
|
1665
1783
|
return false;
|
|
1666
1784
|
if (process.env.WSL_DISTRO_NAME)
|
|
1667
1785
|
return true;
|
|
1668
|
-
return os.release().toLowerCase().includes(
|
|
1786
|
+
return os.release().toLowerCase().includes("microsoft");
|
|
1669
1787
|
}
|
|
1670
1788
|
function extractConversationIdFromUrl(url) {
|
|
1671
1789
|
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
@@ -1675,9 +1793,9 @@ async function resolveUserDataBaseDir() {
|
|
|
1675
1793
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
1676
1794
|
if (isWsl()) {
|
|
1677
1795
|
const candidates = [
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1796
|
+
"/mnt/c/Users/Public/AppData/Local/Temp",
|
|
1797
|
+
"/mnt/c/Temp",
|
|
1798
|
+
"/mnt/c/Windows/Temp",
|
|
1681
1799
|
];
|
|
1682
1800
|
for (const candidate of candidates) {
|
|
1683
1801
|
try {
|
|
@@ -1689,18 +1807,49 @@ async function resolveUserDataBaseDir() {
|
|
|
1689
1807
|
}
|
|
1690
1808
|
}
|
|
1691
1809
|
}
|
|
1692
|
-
|
|
1810
|
+
const tmpDir = os.tmpdir();
|
|
1811
|
+
if (shouldPreferSystemTmpDir(process.platform, tmpDir, os.homedir())) {
|
|
1812
|
+
try {
|
|
1813
|
+
await mkdir("/tmp", { recursive: true });
|
|
1814
|
+
return "/tmp";
|
|
1815
|
+
}
|
|
1816
|
+
catch {
|
|
1817
|
+
// Fall back to the inherited tmpdir if /tmp is unavailable.
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return tmpDir;
|
|
1821
|
+
}
|
|
1822
|
+
function shouldPreferSystemTmpDir(platform, tmpDir, homeDir) {
|
|
1823
|
+
if (platform !== "linux" || !tmpDir || !homeDir)
|
|
1824
|
+
return false;
|
|
1825
|
+
const relativeToHome = path.relative(homeDir, tmpDir);
|
|
1826
|
+
if (!relativeToHome || relativeToHome.startsWith("..") || path.isAbsolute(relativeToHome)) {
|
|
1827
|
+
return false;
|
|
1828
|
+
}
|
|
1829
|
+
const firstSegment = relativeToHome.split(path.sep, 1)[0];
|
|
1830
|
+
return Boolean(firstSegment?.startsWith("."));
|
|
1831
|
+
}
|
|
1832
|
+
export function shouldPreferSystemTmpDirForTest(platform, tmpDir, homeDir) {
|
|
1833
|
+
return shouldPreferSystemTmpDir(platform, tmpDir, homeDir);
|
|
1693
1834
|
}
|
|
1694
1835
|
function buildThinkingStatusExpression() {
|
|
1695
1836
|
const selectors = [
|
|
1696
|
-
|
|
1697
|
-
|
|
1837
|
+
"span.loading-shimmer",
|
|
1838
|
+
"span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary",
|
|
1698
1839
|
'[data-testid*="thinking"]',
|
|
1699
1840
|
'[data-testid*="reasoning"]',
|
|
1700
1841
|
'[role="status"]',
|
|
1701
1842
|
'[aria-live="polite"]',
|
|
1702
1843
|
];
|
|
1703
|
-
const keywords = [
|
|
1844
|
+
const keywords = [
|
|
1845
|
+
"pro thinking",
|
|
1846
|
+
"thinking",
|
|
1847
|
+
"reasoning",
|
|
1848
|
+
"clarifying",
|
|
1849
|
+
"planning",
|
|
1850
|
+
"drafting",
|
|
1851
|
+
"summarizing",
|
|
1852
|
+
];
|
|
1704
1853
|
const selectorLiteral = JSON.stringify(selectors);
|
|
1705
1854
|
const keywordsLiteral = JSON.stringify(keywords);
|
|
1706
1855
|
return `(() => {
|