@steipete/oracle 0.9.0 → 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 +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- 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 +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- 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 +390 -295
- 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 +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- 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 +62 -48
- 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 +2 -2
- 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 +3 -3
- 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 +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- 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 +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- 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 +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- 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 +16 -13
- package/dist/src/oracle/run.js +156 -134
- 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 +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- 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/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
|
@@ -1,28 +1,39 @@
|
|
|
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, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from
|
|
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
|
-
import { runProviderSubmissionFlow } from
|
|
19
|
-
import { chatgptDomProvider } from
|
|
20
|
-
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from
|
|
21
|
-
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from
|
|
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
|
+
}
|
|
22
33
|
function isCloudflareChallengeError(error) {
|
|
23
34
|
if (!(error instanceof BrowserAutomationError))
|
|
24
35
|
return false;
|
|
25
|
-
return error.details?.stage ===
|
|
36
|
+
return error.details?.stage === "cloudflare-challenge";
|
|
26
37
|
}
|
|
27
38
|
function shouldPreserveBrowserOnError(error, headless) {
|
|
28
39
|
return !headless && isCloudflareChallengeError(error);
|
|
@@ -30,10 +41,46 @@ function shouldPreserveBrowserOnError(error, headless) {
|
|
|
30
41
|
export function shouldPreserveBrowserOnErrorForTest(error, headless) {
|
|
31
42
|
return shouldPreserveBrowserOnError(error, headless);
|
|
32
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
|
+
}
|
|
33
80
|
export async function runBrowserMode(options) {
|
|
34
81
|
const promptText = options.prompt?.trim();
|
|
35
82
|
if (!promptText) {
|
|
36
|
-
throw new Error(
|
|
83
|
+
throw new Error("Prompt text is required when using browser mode.");
|
|
37
84
|
}
|
|
38
85
|
const attachments = options.attachments ?? [];
|
|
39
86
|
const fallbackSubmission = options.fallbackSubmission;
|
|
@@ -71,9 +118,9 @@ export async function runBrowserMode(options) {
|
|
|
71
118
|
logger(`Failed to persist runtime hint: ${message}`);
|
|
72
119
|
}
|
|
73
120
|
};
|
|
74
|
-
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
121
|
+
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
75
122
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
76
|
-
...config,
|
|
123
|
+
...redactBrowserConfigForDebugLog(config),
|
|
77
124
|
promptLength: promptText.length,
|
|
78
125
|
})}`);
|
|
79
126
|
}
|
|
@@ -89,18 +136,18 @@ export async function runBrowserMode(options) {
|
|
|
89
136
|
if (config.remoteChrome) {
|
|
90
137
|
// Warn about ignored local-only options
|
|
91
138
|
if (config.headless || config.hideWindow || config.keepBrowser || config.chromePath) {
|
|
92
|
-
logger(
|
|
93
|
-
|
|
139
|
+
logger("Note: --remote-chrome ignores local Chrome flags " +
|
|
140
|
+
"(--browser-headless, --browser-hide-window, --browser-keep-browser, --browser-chrome-path).");
|
|
94
141
|
}
|
|
95
142
|
return runRemoteBrowserMode(promptText, attachments, config, logger, options);
|
|
96
143
|
}
|
|
97
144
|
const manualLogin = Boolean(config.manualLogin);
|
|
98
145
|
const manualProfileDir = config.manualLoginProfileDir
|
|
99
146
|
? path.resolve(config.manualLoginProfileDir)
|
|
100
|
-
: path.join(os.homedir(),
|
|
147
|
+
: path.join(os.homedir(), ".oracle", "browser-profile");
|
|
101
148
|
const userDataDir = manualLogin
|
|
102
149
|
? manualProfileDir
|
|
103
|
-
: await mkdtemp(path.join(await resolveUserDataBaseDir(),
|
|
150
|
+
: await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
|
|
104
151
|
if (manualLogin) {
|
|
105
152
|
// Learned: manual login reuses a persistent profile so cookies/SSO survive.
|
|
106
153
|
await mkdir(userDataDir, { recursive: true });
|
|
@@ -111,14 +158,16 @@ export async function runBrowserMode(options) {
|
|
|
111
158
|
}
|
|
112
159
|
const effectiveKeepBrowser = Boolean(config.keepBrowser);
|
|
113
160
|
const reusedChrome = manualLogin
|
|
114
|
-
? await maybeReuseRunningChrome(userDataDir, logger, {
|
|
161
|
+
? await maybeReuseRunningChrome(userDataDir, logger, {
|
|
162
|
+
waitForPortMs: config.reuseChromeWaitMs,
|
|
163
|
+
})
|
|
115
164
|
: null;
|
|
116
165
|
const chrome = reusedChrome ??
|
|
117
166
|
(await launchChrome({
|
|
118
167
|
...config,
|
|
119
168
|
remoteChrome: config.remoteChrome,
|
|
120
169
|
}, userDataDir, logger));
|
|
121
|
-
const chromeHost = chrome.host ??
|
|
170
|
+
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
122
171
|
// Persist profile state so future manual-login runs can reuse this Chrome.
|
|
123
172
|
if (manualLogin && chrome.port) {
|
|
124
173
|
await writeDevToolsActivePort(userDataDir, chrome.port);
|
|
@@ -129,7 +178,7 @@ export async function runBrowserMode(options) {
|
|
|
129
178
|
let removeTerminationHooks = null;
|
|
130
179
|
try {
|
|
131
180
|
removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
|
|
132
|
-
isInFlight: () => runStatus !==
|
|
181
|
+
isInFlight: () => runStatus !== "complete",
|
|
133
182
|
emitRuntimeHint,
|
|
134
183
|
preserveUserDataDir: manualLogin,
|
|
135
184
|
});
|
|
@@ -140,10 +189,10 @@ export async function runBrowserMode(options) {
|
|
|
140
189
|
let client = null;
|
|
141
190
|
let isolatedTargetId = null;
|
|
142
191
|
const startedAt = Date.now();
|
|
143
|
-
let answerText =
|
|
144
|
-
let answerMarkdown =
|
|
145
|
-
let answerHtml =
|
|
146
|
-
let runStatus =
|
|
192
|
+
let answerText = "";
|
|
193
|
+
let answerMarkdown = "";
|
|
194
|
+
let answerHtml = "";
|
|
195
|
+
let runStatus = "attempted";
|
|
147
196
|
let connectionClosedUnexpectedly = false;
|
|
148
197
|
let stopThinkingMonitor = null;
|
|
149
198
|
let removeDialogHandler = null;
|
|
@@ -152,7 +201,7 @@ export async function runBrowserMode(options) {
|
|
|
152
201
|
try {
|
|
153
202
|
try {
|
|
154
203
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
155
|
-
const connection = await connectWithNewTab(chrome.port, logger,
|
|
204
|
+
const connection = await connectWithNewTab(chrome.port, logger, config.url, chromeHost, {
|
|
156
205
|
fallbackToDefault: !strictTabIsolation,
|
|
157
206
|
retries: strictTabIsolation ? 3 : 0,
|
|
158
207
|
retryDelayMs: 500,
|
|
@@ -168,10 +217,10 @@ export async function runBrowserMode(options) {
|
|
|
168
217
|
throw error;
|
|
169
218
|
}
|
|
170
219
|
const disconnectPromise = new Promise((_, reject) => {
|
|
171
|
-
client?.on(
|
|
220
|
+
client?.on("disconnect", () => {
|
|
172
221
|
connectionClosedUnexpectedly = true;
|
|
173
|
-
logger(
|
|
174
|
-
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."));
|
|
175
224
|
});
|
|
176
225
|
});
|
|
177
226
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
@@ -180,7 +229,7 @@ export async function runBrowserMode(options) {
|
|
|
180
229
|
await hideChromeWindow(chrome, logger);
|
|
181
230
|
}
|
|
182
231
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
183
|
-
if (DOM && typeof DOM.enable ===
|
|
232
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
184
233
|
domainEnablers.push(DOM.enable());
|
|
185
234
|
}
|
|
186
235
|
await Promise.all(domainEnablers);
|
|
@@ -192,13 +241,13 @@ export async function runBrowserMode(options) {
|
|
|
192
241
|
const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
|
|
193
242
|
if (cookieSyncEnabled) {
|
|
194
243
|
if (manualLoginCookieSync) {
|
|
195
|
-
logger(
|
|
244
|
+
logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
|
|
196
245
|
}
|
|
197
246
|
if (!config.inlineCookies) {
|
|
198
|
-
logger(
|
|
247
|
+
logger("Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.");
|
|
199
248
|
}
|
|
200
249
|
else {
|
|
201
|
-
logger(
|
|
250
|
+
logger("Applying inline cookies (skipping Chrome profile read and Keychain prompt)");
|
|
202
251
|
}
|
|
203
252
|
// Learned: always sync cookies before the first navigation so /backend-api/me succeeds.
|
|
204
253
|
const cookieCount = await syncCookies(Network, config.url, config.chromeProfile, logger, {
|
|
@@ -210,32 +259,32 @@ export async function runBrowserMode(options) {
|
|
|
210
259
|
});
|
|
211
260
|
appliedCookies = cookieCount;
|
|
212
261
|
if (config.inlineCookies && cookieCount === 0) {
|
|
213
|
-
throw new Error(
|
|
262
|
+
throw new Error("No inline cookies were applied; aborting before navigation.");
|
|
214
263
|
}
|
|
215
264
|
logger(cookieCount > 0
|
|
216
265
|
? config.inlineCookies
|
|
217
266
|
? `Applied ${cookieCount} inline cookies`
|
|
218
|
-
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ??
|
|
267
|
+
: `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? "Default"}`
|
|
219
268
|
: config.inlineCookies
|
|
220
|
-
?
|
|
221
|
-
:
|
|
269
|
+
? "No inline cookies applied; continuing without session reuse"
|
|
270
|
+
: "No Chrome cookies found; continuing without session reuse");
|
|
222
271
|
}
|
|
223
272
|
else {
|
|
224
273
|
logger(manualLogin
|
|
225
|
-
?
|
|
226
|
-
:
|
|
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)");
|
|
227
276
|
}
|
|
228
277
|
if (cookieSyncEnabled && !manualLogin && (appliedCookies ?? 0) === 0 && !config.inlineCookies) {
|
|
229
278
|
// Learned: if the profile has no ChatGPT cookies, browser mode will just bounce to login.
|
|
230
279
|
// Fail early so the user knows to sign in.
|
|
231
|
-
throw new BrowserAutomationError(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
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",
|
|
235
284
|
details: {
|
|
236
|
-
profile: config.chromeProfile ??
|
|
285
|
+
profile: config.chromeProfile ?? "Default",
|
|
237
286
|
cookiePath: config.chromeCookiePath ?? null,
|
|
238
|
-
hint:
|
|
287
|
+
hint: "If macOS Keychain prompts or denies access, run oracle from a GUI session or use --copy/--render for the manual flow.",
|
|
239
288
|
},
|
|
240
289
|
});
|
|
241
290
|
}
|
|
@@ -245,7 +294,13 @@ export async function runBrowserMode(options) {
|
|
|
245
294
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, baseUrl, logger));
|
|
246
295
|
await raceWithDisconnect(ensureNotBlocked(Runtime, config.headless, logger));
|
|
247
296
|
// Learned: login checks must happen on the base domain before jumping into project URLs.
|
|
248
|
-
await raceWithDisconnect(waitForLogin({
|
|
297
|
+
await raceWithDisconnect(waitForLogin({
|
|
298
|
+
runtime: Runtime,
|
|
299
|
+
logger,
|
|
300
|
+
appliedCookies,
|
|
301
|
+
manualLogin,
|
|
302
|
+
timeoutMs: config.timeoutMs,
|
|
303
|
+
}));
|
|
249
304
|
if (config.url !== baseUrl) {
|
|
250
305
|
await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
|
|
251
306
|
url: config.url,
|
|
@@ -272,10 +327,10 @@ export async function runBrowserMode(options) {
|
|
|
272
327
|
}
|
|
273
328
|
try {
|
|
274
329
|
const { result } = await Runtime.evaluate({
|
|
275
|
-
expression:
|
|
330
|
+
expression: "location.href",
|
|
276
331
|
returnByValue: true,
|
|
277
332
|
});
|
|
278
|
-
if (typeof result?.value ===
|
|
333
|
+
if (typeof result?.value === "string") {
|
|
279
334
|
lastUrl = result.value;
|
|
280
335
|
}
|
|
281
336
|
}
|
|
@@ -286,7 +341,7 @@ export async function runBrowserMode(options) {
|
|
|
286
341
|
logger(`[browser] url = ${lastUrl}`);
|
|
287
342
|
}
|
|
288
343
|
if (chrome?.port) {
|
|
289
|
-
const suffix = lastTargetId ? ` target=${lastTargetId}` :
|
|
344
|
+
const suffix = lastTargetId ? ` target=${lastTargetId}` : "";
|
|
290
345
|
if (lastUrl) {
|
|
291
346
|
logger(`[reattach] chrome port=${chrome.port} host=${chromeHost} url=${lastUrl}${suffix}`);
|
|
292
347
|
}
|
|
@@ -304,8 +359,11 @@ export async function runBrowserMode(options) {
|
|
|
304
359
|
const start = Date.now();
|
|
305
360
|
while (Date.now() - start < timeoutMs) {
|
|
306
361
|
try {
|
|
307
|
-
const { result } = await Runtime.evaluate({
|
|
308
|
-
|
|
362
|
+
const { result } = await Runtime.evaluate({
|
|
363
|
+
expression: "location.href",
|
|
364
|
+
returnByValue: true,
|
|
365
|
+
});
|
|
366
|
+
if (typeof result?.value === "string" && result.value.includes("/c/")) {
|
|
309
367
|
lastUrl = result.value;
|
|
310
368
|
logger(`[browser] conversation url (${label}) = ${lastUrl}`);
|
|
311
369
|
await emitRuntimeHint();
|
|
@@ -333,7 +391,7 @@ export async function runBrowserMode(options) {
|
|
|
333
391
|
};
|
|
334
392
|
await captureRuntimeSnapshot();
|
|
335
393
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
336
|
-
if (config.desiredModel && modelStrategy !==
|
|
394
|
+
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
337
395
|
await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
338
396
|
retries: 2,
|
|
339
397
|
delayMs: 300,
|
|
@@ -345,15 +403,15 @@ export async function runBrowserMode(options) {
|
|
|
345
403
|
})).catch((error) => {
|
|
346
404
|
const base = error instanceof Error ? error.message : String(error);
|
|
347
405
|
const hint = appliedCookies === 0
|
|
348
|
-
?
|
|
349
|
-
:
|
|
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
|
+
: "";
|
|
350
408
|
throw new Error(`${base}${hint}`);
|
|
351
409
|
});
|
|
352
410
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
353
411
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
354
412
|
}
|
|
355
|
-
else if (modelStrategy ===
|
|
356
|
-
logger(
|
|
413
|
+
else if (modelStrategy === "ignore") {
|
|
414
|
+
logger("Model picker: skipped (strategy=ignore)");
|
|
357
415
|
}
|
|
358
416
|
// Handle thinking time selection if specified
|
|
359
417
|
const thinkingTime = config.thinkingTime;
|
|
@@ -387,13 +445,12 @@ export async function runBrowserMode(options) {
|
|
|
387
445
|
};
|
|
388
446
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
389
447
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
390
|
-
const baselineAssistantText = typeof baselineSnapshot?.text ===
|
|
448
|
+
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
391
449
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
392
|
-
let attachmentWaitTimedOut = false;
|
|
393
450
|
let inputOnlyAttachments = false;
|
|
394
451
|
if (submissionAttachments.length > 0) {
|
|
395
452
|
if (!DOM) {
|
|
396
|
-
throw new Error(
|
|
453
|
+
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
397
454
|
}
|
|
398
455
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
399
456
|
for (let attachmentIndex = 0; attachmentIndex < submissionAttachments.length; attachmentIndex += 1) {
|
|
@@ -409,24 +466,11 @@ export async function runBrowserMode(options) {
|
|
|
409
466
|
const baseTimeout = config.inputTimeoutMs ?? 30_000;
|
|
410
467
|
const perFileTimeout = 20_000;
|
|
411
468
|
const waitBudget = Math.max(baseTimeout, 45_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
logger('All attachments uploaded');
|
|
415
|
-
}
|
|
416
|
-
catch (error) {
|
|
417
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
418
|
-
if (/Attachments did not finish uploading before timeout/i.test(message)) {
|
|
419
|
-
attachmentWaitTimedOut = true;
|
|
420
|
-
logger(`[browser] Attachment upload timed out after ${Math.round(waitBudget / 1000)}s; continuing without confirmation.`);
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
throw error;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
469
|
+
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
470
|
+
logger("All attachments uploaded");
|
|
426
471
|
}
|
|
427
472
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
428
473
|
// Learned: return baselineTurns so assistant polling can ignore earlier content.
|
|
429
|
-
const sendAttachmentNames = attachmentWaitTimedOut ? [] : attachmentNames;
|
|
430
474
|
const providerState = {
|
|
431
475
|
runtime: Runtime,
|
|
432
476
|
input: Input,
|
|
@@ -434,7 +478,7 @@ export async function runBrowserMode(options) {
|
|
|
434
478
|
timeoutMs: config.timeoutMs,
|
|
435
479
|
inputTimeoutMs: config.inputTimeoutMs ?? undefined,
|
|
436
480
|
baselineTurns: baselineTurns ?? undefined,
|
|
437
|
-
attachmentNames
|
|
481
|
+
attachmentNames,
|
|
438
482
|
};
|
|
439
483
|
await runProviderSubmissionFlow(chatgptDomProvider, {
|
|
440
484
|
prompt,
|
|
@@ -444,76 +488,82 @@ export async function runBrowserMode(options) {
|
|
|
444
488
|
state: providerState,
|
|
445
489
|
});
|
|
446
490
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
447
|
-
if (typeof providerBaselineTurns ===
|
|
491
|
+
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
448
492
|
baselineTurns = providerBaselineTurns;
|
|
449
493
|
}
|
|
450
494
|
if (attachmentNames.length > 0) {
|
|
451
|
-
if (
|
|
452
|
-
logger(
|
|
453
|
-
}
|
|
454
|
-
else if (inputOnlyAttachments) {
|
|
455
|
-
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.");
|
|
456
497
|
}
|
|
457
498
|
else {
|
|
458
|
-
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
|
+
});
|
|
459
504
|
if (!verified) {
|
|
460
|
-
|
|
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");
|
|
461
509
|
}
|
|
462
|
-
logger('Verified attachments present on sent user message');
|
|
463
510
|
}
|
|
464
511
|
}
|
|
465
512
|
// Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
|
|
466
|
-
scheduleConversationHint(
|
|
513
|
+
scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
|
|
467
514
|
return { baselineTurns, baselineAssistantText };
|
|
468
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
|
+
};
|
|
469
521
|
let baselineTurns = null;
|
|
470
522
|
let baselineAssistantText = null;
|
|
471
523
|
await acquireProfileLockIfNeeded();
|
|
472
524
|
try {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
error.details?.code === 'prompt-too-large';
|
|
481
|
-
if (fallbackSubmission && isPromptTooLarge) {
|
|
482
|
-
// Learned: when prompts truncate, retry with file uploads so the UI receives the full content.
|
|
483
|
-
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 () => {
|
|
484
532
|
await raceWithDisconnect(clearPromptComposer(Runtime, logger));
|
|
485
533
|
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
534
|
+
},
|
|
535
|
+
logger,
|
|
536
|
+
});
|
|
537
|
+
baselineTurns = submission.baselineTurns;
|
|
538
|
+
baselineAssistantText = submission.baselineAssistantText;
|
|
494
539
|
}
|
|
495
540
|
finally {
|
|
496
541
|
await releaseProfileLockIfHeld();
|
|
497
542
|
}
|
|
498
543
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
499
544
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
500
|
-
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;
|
|
501
547
|
const waitForFreshAssistantResponse = async (baselineNormalized, timeoutMs) => {
|
|
502
548
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
503
549
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
504
|
-
:
|
|
550
|
+
: "";
|
|
505
551
|
const deadline = Date.now() + timeoutMs;
|
|
506
552
|
while (Date.now() < deadline) {
|
|
507
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
508
|
-
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() : "";
|
|
509
555
|
if (text) {
|
|
510
556
|
const normalized = normalizeForComparison(text);
|
|
511
|
-
const isBaseline = normalized === baselineNormalized ||
|
|
557
|
+
const isBaseline = normalized === baselineNormalized ||
|
|
558
|
+
(baselinePrefix.length > 0 && normalized.startsWith(baselinePrefix));
|
|
512
559
|
if (!isBaseline) {
|
|
513
560
|
return {
|
|
514
561
|
text,
|
|
515
562
|
html: snapshot?.html ?? undefined,
|
|
516
|
-
meta: {
|
|
563
|
+
meta: {
|
|
564
|
+
turnId: snapshot?.turnId ?? undefined,
|
|
565
|
+
messageId: snapshot?.messageId ?? undefined,
|
|
566
|
+
},
|
|
517
567
|
};
|
|
518
568
|
}
|
|
519
569
|
}
|
|
@@ -529,7 +579,7 @@ export async function runBrowserMode(options) {
|
|
|
529
579
|
return null;
|
|
530
580
|
logger(`[browser] Assistant response timed out; waiting ${formatElapsed(recheckDelayMs)} before rechecking conversation.`);
|
|
531
581
|
await raceWithDisconnect(delay(recheckDelayMs));
|
|
532
|
-
await updateConversationHint(
|
|
582
|
+
await updateConversationHint("assistant-recheck", 15_000).catch(() => false);
|
|
533
583
|
await captureRuntimeSnapshot().catch(() => undefined);
|
|
534
584
|
const conversationUrl = await readConversationUrl(Runtime);
|
|
535
585
|
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
@@ -544,12 +594,12 @@ export async function runBrowserMode(options) {
|
|
|
544
594
|
// Update session metadata to indicate login is needed
|
|
545
595
|
await emitRuntimeHint();
|
|
546
596
|
throw new BrowserAutomationError(`ChatGPT session expired during recheck: ${sessionValid.reason}. ` +
|
|
547
|
-
`Conversation URL: ${conversationUrl || lastUrl ||
|
|
597
|
+
`Conversation URL: ${conversationUrl || lastUrl || "unknown"}. ` +
|
|
548
598
|
`Please sign in and retry.`, {
|
|
549
|
-
stage:
|
|
599
|
+
stage: "assistant-recheck",
|
|
550
600
|
details: {
|
|
551
601
|
conversationUrl: conversationUrl || lastUrl || null,
|
|
552
|
-
sessionStatus:
|
|
602
|
+
sessionStatus: "needs_login",
|
|
553
603
|
validationReason: sessionValid.reason,
|
|
554
604
|
},
|
|
555
605
|
runtime: {
|
|
@@ -565,12 +615,13 @@ export async function runBrowserMode(options) {
|
|
|
565
615
|
});
|
|
566
616
|
}
|
|
567
617
|
const timeoutMs = recheckTimeoutMs > 0 ? recheckTimeoutMs : config.timeoutMs;
|
|
568
|
-
const rechecked = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined));
|
|
569
|
-
logger(
|
|
618
|
+
const rechecked = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId()));
|
|
619
|
+
logger("Recovered assistant response after delayed recheck");
|
|
570
620
|
return rechecked;
|
|
571
621
|
};
|
|
572
622
|
try {
|
|
573
|
-
|
|
623
|
+
await updateConversationHint("assistant-wait", 15_000).catch(() => false);
|
|
624
|
+
answer = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId()));
|
|
574
625
|
}
|
|
575
626
|
catch (error) {
|
|
576
627
|
if (isAssistantResponseTimeoutError(error)) {
|
|
@@ -579,7 +630,7 @@ export async function runBrowserMode(options) {
|
|
|
579
630
|
answer = rechecked;
|
|
580
631
|
}
|
|
581
632
|
else {
|
|
582
|
-
await updateConversationHint(
|
|
633
|
+
await updateConversationHint("assistant-timeout", 15_000).catch(() => false);
|
|
583
634
|
await captureRuntimeSnapshot().catch(() => undefined);
|
|
584
635
|
const runtime = {
|
|
585
636
|
chromePid: chrome.pid,
|
|
@@ -591,7 +642,7 @@ export async function runBrowserMode(options) {
|
|
|
591
642
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
592
643
|
controllerPid: process.pid,
|
|
593
644
|
};
|
|
594
|
-
throw new BrowserAutomationError(
|
|
645
|
+
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime }, error);
|
|
595
646
|
}
|
|
596
647
|
}
|
|
597
648
|
else {
|
|
@@ -599,17 +650,19 @@ export async function runBrowserMode(options) {
|
|
|
599
650
|
}
|
|
600
651
|
}
|
|
601
652
|
// Ensure we store the final conversation URL even if the UI updated late.
|
|
602
|
-
await updateConversationHint(
|
|
603
|
-
const baselineNormalized = baselineAssistantText
|
|
653
|
+
await updateConversationHint("post-response", 15_000);
|
|
654
|
+
const baselineNormalized = baselineAssistantText
|
|
655
|
+
? normalizeForComparison(baselineAssistantText)
|
|
656
|
+
: "";
|
|
604
657
|
if (baselineNormalized) {
|
|
605
|
-
const normalizedAnswer = normalizeForComparison(answer.text ??
|
|
658
|
+
const normalizedAnswer = normalizeForComparison(answer.text ?? "");
|
|
606
659
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
607
660
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
608
|
-
:
|
|
661
|
+
: "";
|
|
609
662
|
const isBaseline = normalizedAnswer === baselineNormalized ||
|
|
610
663
|
(baselinePrefix.length > 0 && normalizedAnswer.startsWith(baselinePrefix));
|
|
611
664
|
if (isBaseline) {
|
|
612
|
-
logger(
|
|
665
|
+
logger("Detected stale assistant response; waiting for new response...");
|
|
613
666
|
const refreshed = await waitForFreshAssistantResponse(baselineNormalized, 15_000);
|
|
614
667
|
if (refreshed) {
|
|
615
668
|
answer = refreshed;
|
|
@@ -617,11 +670,11 @@ export async function runBrowserMode(options) {
|
|
|
617
670
|
}
|
|
618
671
|
}
|
|
619
672
|
answerText = answer.text;
|
|
620
|
-
answerHtml = answer.html ??
|
|
673
|
+
answerHtml = answer.html ?? "";
|
|
621
674
|
const copiedMarkdown = await raceWithDisconnect(withRetries(async () => {
|
|
622
675
|
const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
|
|
623
676
|
if (!attempt) {
|
|
624
|
-
throw new Error(
|
|
677
|
+
throw new Error("copy-missing");
|
|
625
678
|
}
|
|
626
679
|
return attempt;
|
|
627
680
|
}, {
|
|
@@ -644,8 +697,8 @@ export async function runBrowserMode(options) {
|
|
|
644
697
|
allowMarkdownUpdate: !copiedMarkdown,
|
|
645
698
|
}));
|
|
646
699
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
647
|
-
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
648
|
-
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() : "";
|
|
649
702
|
if (finalText && finalText !== promptText.trim()) {
|
|
650
703
|
const trimmedMarkdown = answerMarkdown.trim();
|
|
651
704
|
const finalIsEcho = promptEchoMatcher ? promptEchoMatcher.isEcho(finalText) : false;
|
|
@@ -655,27 +708,27 @@ export async function runBrowserMode(options) {
|
|
|
655
708
|
trimmedMarkdown.length > 0 &&
|
|
656
709
|
lengthDelta >= Math.max(12, Math.floor(trimmedMarkdown.length * 0.75));
|
|
657
710
|
if ((missingCopy || likelyTruncatedCopy) && !finalIsEcho && finalText !== trimmedMarkdown) {
|
|
658
|
-
logger(
|
|
711
|
+
logger("Refreshed assistant response via final DOM snapshot");
|
|
659
712
|
answerText = finalText;
|
|
660
713
|
answerMarkdown = finalText;
|
|
661
714
|
}
|
|
662
715
|
}
|
|
663
716
|
// Detect prompt echo using normalized comparison (whitespace-insensitive).
|
|
664
717
|
const alignedEcho = alignPromptEchoPair(answerText, answerMarkdown, promptEchoMatcher, copiedMarkdown ? logger : undefined, {
|
|
665
|
-
text:
|
|
666
|
-
markdown:
|
|
718
|
+
text: "Aligned assistant response text to copied markdown after prompt echo",
|
|
719
|
+
markdown: "Aligned assistant markdown to response text after prompt echo",
|
|
667
720
|
});
|
|
668
721
|
answerText = alignedEcho.answerText;
|
|
669
722
|
answerMarkdown = alignedEcho.answerMarkdown;
|
|
670
723
|
const isPromptEcho = alignedEcho.isEcho;
|
|
671
724
|
if (isPromptEcho) {
|
|
672
|
-
logger(
|
|
725
|
+
logger("Detected prompt echo in response; waiting for actual assistant response...");
|
|
673
726
|
const deadline = Date.now() + 15_000;
|
|
674
727
|
let bestText = null;
|
|
675
728
|
let stableCount = 0;
|
|
676
729
|
while (Date.now() < deadline) {
|
|
677
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
678
|
-
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() : "";
|
|
679
732
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
680
733
|
if (!isStillEcho) {
|
|
681
734
|
if (!bestText || text.length > bestText.length) {
|
|
@@ -692,7 +745,7 @@ export async function runBrowserMode(options) {
|
|
|
692
745
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
693
746
|
}
|
|
694
747
|
if (bestText) {
|
|
695
|
-
logger(
|
|
748
|
+
logger("Recovered assistant response after detecting prompt echo");
|
|
696
749
|
answerText = bestText;
|
|
697
750
|
answerMarkdown = bestText;
|
|
698
751
|
}
|
|
@@ -703,8 +756,8 @@ export async function runBrowserMode(options) {
|
|
|
703
756
|
let bestText = answerText.trim();
|
|
704
757
|
let stableCycles = 0;
|
|
705
758
|
while (Date.now() < deadline) {
|
|
706
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
707
|
-
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() : "";
|
|
708
761
|
if (text && text.length > bestText.length) {
|
|
709
762
|
bestText = text;
|
|
710
763
|
stableCycles = 0;
|
|
@@ -718,17 +771,17 @@ export async function runBrowserMode(options) {
|
|
|
718
771
|
await delay(400);
|
|
719
772
|
}
|
|
720
773
|
if (bestText.length > answerText.trim().length) {
|
|
721
|
-
logger(
|
|
774
|
+
logger("Refreshed short assistant response from latest DOM snapshot");
|
|
722
775
|
answerText = bestText;
|
|
723
776
|
answerMarkdown = bestText;
|
|
724
777
|
}
|
|
725
778
|
}
|
|
726
779
|
if (connectionClosedUnexpectedly) {
|
|
727
780
|
// Bail out on mid-run disconnects so the session stays reattachable.
|
|
728
|
-
throw new Error(
|
|
781
|
+
throw new Error("Chrome disconnected before completion");
|
|
729
782
|
}
|
|
730
783
|
stopThinkingMonitor?.();
|
|
731
|
-
runStatus =
|
|
784
|
+
runStatus = "complete";
|
|
732
785
|
const durationMs = Date.now() - startedAt;
|
|
733
786
|
const answerChars = answerText.length;
|
|
734
787
|
const answerTokens = estimateTokenCount(answerMarkdown);
|
|
@@ -767,28 +820,28 @@ export async function runBrowserMode(options) {
|
|
|
767
820
|
const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
|
|
768
821
|
`--browser-manual-login-profile-dir ${JSON.stringify(userDataDir)}`;
|
|
769
822
|
await emitRuntimeHint();
|
|
770
|
-
logger(
|
|
823
|
+
logger("Cloudflare challenge detected; leaving browser open so you can complete the check.");
|
|
771
824
|
logger(`Reuse this browser profile with: ${reuseProfileHint}`);
|
|
772
|
-
throw new BrowserAutomationError(
|
|
773
|
-
stage:
|
|
825
|
+
throw new BrowserAutomationError("Cloudflare challenge detected. Complete the “Just a moment…” check in the open browser, then rerun.", {
|
|
826
|
+
stage: "cloudflare-challenge",
|
|
774
827
|
runtime,
|
|
775
828
|
reuseProfileHint,
|
|
776
829
|
}, normalizedError);
|
|
777
830
|
}
|
|
778
831
|
if (!socketClosed) {
|
|
779
832
|
logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
|
|
780
|
-
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
833
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
781
834
|
logger(normalizedError.stack);
|
|
782
835
|
}
|
|
783
836
|
throw normalizedError;
|
|
784
837
|
}
|
|
785
|
-
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
838
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
786
839
|
logger(`Chrome window closed before completion: ${normalizedError.message}`);
|
|
787
840
|
logger(normalizedError.stack);
|
|
788
841
|
}
|
|
789
842
|
await emitRuntimeHint();
|
|
790
|
-
throw new BrowserAutomationError(
|
|
791
|
-
stage:
|
|
843
|
+
throw new BrowserAutomationError("Chrome window closed before oracle finished. Please keep it open until completion.", {
|
|
844
|
+
stage: "connection-lost",
|
|
792
845
|
runtime: {
|
|
793
846
|
chromePid: chrome.pid,
|
|
794
847
|
chromePort: chrome.port,
|
|
@@ -812,7 +865,7 @@ export async function runBrowserMode(options) {
|
|
|
812
865
|
// Close the isolated tab once the response has been fully captured to prevent
|
|
813
866
|
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
814
867
|
// so reattach can recover the response.
|
|
815
|
-
if (runStatus ===
|
|
868
|
+
if (runStatus === "complete" && isolatedTargetId && chrome?.port) {
|
|
816
869
|
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
817
870
|
}
|
|
818
871
|
removeDialogHandler?.();
|
|
@@ -834,7 +887,7 @@ export async function runBrowserMode(options) {
|
|
|
834
887
|
});
|
|
835
888
|
if (shouldCleanup) {
|
|
836
889
|
// Preserve the persistent manual-login profile, but clear stale reattach hints.
|
|
837
|
-
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode:
|
|
890
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
|
|
838
891
|
}
|
|
839
892
|
}
|
|
840
893
|
else {
|
|
@@ -866,28 +919,28 @@ async function pickAvailableDebugPort(preferredPort, logger) {
|
|
|
866
919
|
async function isPortAvailable(port) {
|
|
867
920
|
return new Promise((resolve) => {
|
|
868
921
|
const server = net.createServer();
|
|
869
|
-
server.once(
|
|
870
|
-
server.once(
|
|
922
|
+
server.once("error", () => resolve(false));
|
|
923
|
+
server.once("listening", () => {
|
|
871
924
|
server.close(() => resolve(true));
|
|
872
925
|
});
|
|
873
|
-
server.listen(port,
|
|
926
|
+
server.listen(port, "127.0.0.1");
|
|
874
927
|
});
|
|
875
928
|
}
|
|
876
929
|
async function findEphemeralPort() {
|
|
877
930
|
return new Promise((resolve, reject) => {
|
|
878
931
|
const server = net.createServer();
|
|
879
|
-
server.once(
|
|
932
|
+
server.once("error", (error) => {
|
|
880
933
|
server.close();
|
|
881
934
|
reject(error);
|
|
882
935
|
});
|
|
883
|
-
server.listen(0,
|
|
936
|
+
server.listen(0, "127.0.0.1", () => {
|
|
884
937
|
const address = server.address();
|
|
885
|
-
if (address && typeof address ===
|
|
938
|
+
if (address && typeof address === "object") {
|
|
886
939
|
const port = address.port;
|
|
887
940
|
server.close(() => resolve(port));
|
|
888
941
|
}
|
|
889
942
|
else {
|
|
890
|
-
server.close(() => reject(new Error(
|
|
943
|
+
server.close(() => reject(new Error("Failed to acquire ephemeral port")));
|
|
891
944
|
}
|
|
892
945
|
});
|
|
893
946
|
});
|
|
@@ -906,20 +959,20 @@ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, time
|
|
|
906
959
|
}
|
|
907
960
|
catch (error) {
|
|
908
961
|
const message = error instanceof Error ? error.message : String(error);
|
|
909
|
-
const loginDetected = message?.toLowerCase().includes(
|
|
910
|
-
const sessionMissing = message?.toLowerCase().includes(
|
|
962
|
+
const loginDetected = message?.toLowerCase().includes("login button");
|
|
963
|
+
const sessionMissing = message?.toLowerCase().includes("session not detected");
|
|
911
964
|
if (!loginDetected && !sessionMissing) {
|
|
912
965
|
throw error;
|
|
913
966
|
}
|
|
914
967
|
const now = Date.now();
|
|
915
968
|
if (now - lastNotice > 5000) {
|
|
916
|
-
logger(
|
|
969
|
+
logger("Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...");
|
|
917
970
|
lastNotice = now;
|
|
918
971
|
}
|
|
919
972
|
await delay(1000);
|
|
920
973
|
}
|
|
921
974
|
}
|
|
922
|
-
throw new Error(
|
|
975
|
+
throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
|
|
923
976
|
}
|
|
924
977
|
async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answerText, answerMarkdown, logger, allowMarkdownUpdate, }) {
|
|
925
978
|
// Learned: long streaming responses can still be rendering after initial capture.
|
|
@@ -933,7 +986,7 @@ async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answe
|
|
|
933
986
|
let bestText = answerText;
|
|
934
987
|
for (let i = 0; i < 5; i++) {
|
|
935
988
|
const laterSnapshot = await readAssistantSnapshot(runtime, baselineTurns ?? undefined).catch(() => null);
|
|
936
|
-
const laterText = typeof laterSnapshot?.text ===
|
|
989
|
+
const laterText = typeof laterSnapshot?.text === "string" ? laterSnapshot.text.trim() : "";
|
|
937
990
|
if (laterText.length > bestLength) {
|
|
938
991
|
bestLength = laterText.length;
|
|
939
992
|
bestText = laterText;
|
|
@@ -954,22 +1007,22 @@ async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answe
|
|
|
954
1007
|
}
|
|
955
1008
|
async function _assertNavigatedToHttp(runtime, _logger, timeoutMs = 10_000) {
|
|
956
1009
|
const deadline = Date.now() + timeoutMs;
|
|
957
|
-
let lastUrl =
|
|
1010
|
+
let lastUrl = "";
|
|
958
1011
|
while (Date.now() < deadline) {
|
|
959
1012
|
const { result } = await runtime.evaluate({
|
|
960
1013
|
expression: 'typeof location === "object" && location.href ? location.href : ""',
|
|
961
1014
|
returnByValue: true,
|
|
962
1015
|
});
|
|
963
|
-
const url = typeof result?.value ===
|
|
1016
|
+
const url = typeof result?.value === "string" ? result.value : "";
|
|
964
1017
|
lastUrl = url;
|
|
965
1018
|
if (/^https?:\/\//i.test(url)) {
|
|
966
1019
|
return url;
|
|
967
1020
|
}
|
|
968
1021
|
await delay(250);
|
|
969
1022
|
}
|
|
970
|
-
throw new BrowserAutomationError(
|
|
971
|
-
stage:
|
|
972
|
-
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)" },
|
|
973
1026
|
});
|
|
974
1027
|
}
|
|
975
1028
|
async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
@@ -989,11 +1042,11 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
|
989
1042
|
if (!probe.ok) {
|
|
990
1043
|
logger(`DevToolsActivePort found for ${userDataDir} but unreachable (${probe.error}); launching new Chrome.`);
|
|
991
1044
|
// Safe cleanup: remove stale DevToolsActivePort; only remove lock files if this was an Oracle-owned pid that died.
|
|
992
|
-
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode:
|
|
1045
|
+
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
|
|
993
1046
|
return null;
|
|
994
1047
|
}
|
|
995
1048
|
const pid = await readChromePid(userDataDir);
|
|
996
|
-
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}` : ""})`);
|
|
997
1050
|
return {
|
|
998
1051
|
port,
|
|
999
1052
|
pid: pid ?? undefined,
|
|
@@ -1004,7 +1057,7 @@ async function maybeReuseRunningChrome(userDataDir, logger, options = {}) {
|
|
|
1004
1057
|
async function runRemoteBrowserMode(promptText, attachments, config, logger, options) {
|
|
1005
1058
|
const remoteChromeConfig = config.remoteChrome;
|
|
1006
1059
|
if (!remoteChromeConfig) {
|
|
1007
|
-
throw new Error(
|
|
1060
|
+
throw new Error("Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.");
|
|
1008
1061
|
}
|
|
1009
1062
|
const { host, port } = remoteChromeConfig;
|
|
1010
1063
|
logger(`Connecting to remote Chrome at ${host}:${port}`);
|
|
@@ -1030,9 +1083,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1030
1083
|
}
|
|
1031
1084
|
};
|
|
1032
1085
|
const startedAt = Date.now();
|
|
1033
|
-
let answerText =
|
|
1034
|
-
let answerMarkdown =
|
|
1035
|
-
let answerHtml =
|
|
1086
|
+
let answerText = "";
|
|
1087
|
+
let answerMarkdown = "";
|
|
1088
|
+
let answerHtml = "";
|
|
1036
1089
|
let connectionClosedUnexpectedly = false;
|
|
1037
1090
|
let stopThinkingMonitor = null;
|
|
1038
1091
|
let removeDialogHandler = null;
|
|
@@ -1044,16 +1097,16 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1044
1097
|
const markConnectionLost = () => {
|
|
1045
1098
|
connectionClosedUnexpectedly = true;
|
|
1046
1099
|
};
|
|
1047
|
-
client.on(
|
|
1100
|
+
client.on("disconnect", markConnectionLost);
|
|
1048
1101
|
const { Network, Page, Runtime, Input, DOM } = client;
|
|
1049
1102
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
1050
|
-
if (DOM && typeof DOM.enable ===
|
|
1103
|
+
if (DOM && typeof DOM.enable === "function") {
|
|
1051
1104
|
domainEnablers.push(DOM.enable());
|
|
1052
1105
|
}
|
|
1053
1106
|
await Promise.all(domainEnablers);
|
|
1054
1107
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
1055
1108
|
// Skip cookie sync for remote Chrome - it already has cookies
|
|
1056
|
-
logger(
|
|
1109
|
+
logger("Skipping cookie sync for remote Chrome (using existing session)");
|
|
1057
1110
|
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
1058
1111
|
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
1059
1112
|
await ensureLoggedIn(Runtime, logger, { remoteSession: true });
|
|
@@ -1061,10 +1114,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1061
1114
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1062
1115
|
try {
|
|
1063
1116
|
const { result } = await Runtime.evaluate({
|
|
1064
|
-
expression:
|
|
1117
|
+
expression: "location.href",
|
|
1065
1118
|
returnByValue: true,
|
|
1066
1119
|
});
|
|
1067
|
-
if (typeof result?.value ===
|
|
1120
|
+
if (typeof result?.value === "string") {
|
|
1068
1121
|
lastUrl = result.value;
|
|
1069
1122
|
}
|
|
1070
1123
|
await emitRuntimeHint();
|
|
@@ -1073,7 +1126,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1073
1126
|
// ignore
|
|
1074
1127
|
}
|
|
1075
1128
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
1076
|
-
if (config.desiredModel && modelStrategy !==
|
|
1129
|
+
if (config.desiredModel && modelStrategy !== "ignore") {
|
|
1077
1130
|
await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
|
|
1078
1131
|
retries: 2,
|
|
1079
1132
|
delayMs: 300,
|
|
@@ -1086,8 +1139,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1086
1139
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
1087
1140
|
logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
|
|
1088
1141
|
}
|
|
1089
|
-
else if (modelStrategy ===
|
|
1090
|
-
logger(
|
|
1142
|
+
else if (modelStrategy === "ignore") {
|
|
1143
|
+
logger("Model picker: skipped (strategy=ignore)");
|
|
1091
1144
|
}
|
|
1092
1145
|
// Handle thinking time selection if specified
|
|
1093
1146
|
const thinkingTime = config.thinkingTime;
|
|
@@ -1104,11 +1157,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1104
1157
|
}
|
|
1105
1158
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
1106
1159
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
1107
|
-
const baselineAssistantText = typeof baselineSnapshot?.text ===
|
|
1160
|
+
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
1108
1161
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
1109
1162
|
if (submissionAttachments.length > 0) {
|
|
1110
1163
|
if (!DOM) {
|
|
1111
|
-
throw new Error(
|
|
1164
|
+
throw new Error("Chrome DOM domain unavailable while uploading attachments.");
|
|
1112
1165
|
}
|
|
1113
1166
|
await clearComposerAttachments(Runtime, 5_000, logger);
|
|
1114
1167
|
// Use remote file transfer for remote Chrome (reads local files and injects via CDP)
|
|
@@ -1122,7 +1175,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1122
1175
|
const perFileTimeout = 15_000;
|
|
1123
1176
|
const waitBudget = Math.max(baseTimeout, 30_000) + (submissionAttachments.length - 1) * perFileTimeout;
|
|
1124
1177
|
await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
|
|
1125
|
-
logger(
|
|
1178
|
+
logger("All attachments uploaded");
|
|
1126
1179
|
}
|
|
1127
1180
|
let baselineTurns = await readConversationTurnCount(Runtime, logger);
|
|
1128
1181
|
const providerState = {
|
|
@@ -1142,52 +1195,56 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1142
1195
|
state: providerState,
|
|
1143
1196
|
});
|
|
1144
1197
|
const providerBaselineTurns = providerState.baselineTurns;
|
|
1145
|
-
if (typeof providerBaselineTurns ===
|
|
1198
|
+
if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
|
|
1146
1199
|
baselineTurns = providerBaselineTurns;
|
|
1147
1200
|
}
|
|
1148
1201
|
return { baselineTurns, baselineAssistantText };
|
|
1149
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
|
+
};
|
|
1150
1208
|
let baselineTurns = null;
|
|
1151
1209
|
let baselineAssistantText = null;
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
error.details?.code === 'prompt-too-large';
|
|
1160
|
-
if (options.fallbackSubmission && isPromptTooLarge) {
|
|
1161
|
-
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 () => {
|
|
1162
1217
|
await clearPromptComposer(Runtime, logger);
|
|
1163
1218
|
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
throw error;
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1219
|
+
},
|
|
1220
|
+
logger,
|
|
1221
|
+
});
|
|
1222
|
+
baselineTurns = submission.baselineTurns;
|
|
1223
|
+
baselineAssistantText = submission.baselineAssistantText;
|
|
1172
1224
|
stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
|
|
1173
1225
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
1174
|
-
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;
|
|
1175
1228
|
const waitForFreshAssistantResponse = async (baselineNormalized, timeoutMs) => {
|
|
1176
1229
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
1177
1230
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
1178
|
-
:
|
|
1231
|
+
: "";
|
|
1179
1232
|
const deadline = Date.now() + timeoutMs;
|
|
1180
1233
|
while (Date.now() < deadline) {
|
|
1181
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1182
|
-
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() : "";
|
|
1183
1236
|
if (text) {
|
|
1184
1237
|
const normalized = normalizeForComparison(text);
|
|
1185
|
-
const isBaseline = normalized === baselineNormalized ||
|
|
1238
|
+
const isBaseline = normalized === baselineNormalized ||
|
|
1239
|
+
(baselinePrefix.length > 0 && normalized.startsWith(baselinePrefix));
|
|
1186
1240
|
if (!isBaseline) {
|
|
1187
1241
|
return {
|
|
1188
1242
|
text,
|
|
1189
1243
|
html: snapshot?.html ?? undefined,
|
|
1190
|
-
meta: {
|
|
1244
|
+
meta: {
|
|
1245
|
+
turnId: snapshot?.turnId ?? undefined,
|
|
1246
|
+
messageId: snapshot?.messageId ?? undefined,
|
|
1247
|
+
},
|
|
1191
1248
|
};
|
|
1192
1249
|
}
|
|
1193
1250
|
}
|
|
@@ -1217,12 +1274,12 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1217
1274
|
// Update session metadata to indicate login is needed
|
|
1218
1275
|
await emitRuntimeHint();
|
|
1219
1276
|
throw new BrowserAutomationError(`ChatGPT session expired during recheck: ${sessionValid.reason}. ` +
|
|
1220
|
-
`Conversation URL: ${conversationUrl || lastUrl ||
|
|
1277
|
+
`Conversation URL: ${conversationUrl || lastUrl || "unknown"}. ` +
|
|
1221
1278
|
`Please sign in and retry.`, {
|
|
1222
|
-
stage:
|
|
1279
|
+
stage: "assistant-recheck",
|
|
1223
1280
|
details: {
|
|
1224
1281
|
conversationUrl: conversationUrl || lastUrl || null,
|
|
1225
|
-
sessionStatus:
|
|
1282
|
+
sessionStatus: "needs_login",
|
|
1226
1283
|
validationReason: sessionValid.reason,
|
|
1227
1284
|
},
|
|
1228
1285
|
runtime: {
|
|
@@ -1237,12 +1294,17 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1237
1294
|
}
|
|
1238
1295
|
await emitRuntimeHint();
|
|
1239
1296
|
const timeoutMs = recheckTimeoutMs > 0 ? recheckTimeoutMs : config.timeoutMs;
|
|
1240
|
-
const rechecked = await waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined);
|
|
1241
|
-
logger(
|
|
1297
|
+
const rechecked = await waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId());
|
|
1298
|
+
logger("Recovered assistant response after delayed recheck");
|
|
1242
1299
|
return rechecked;
|
|
1243
1300
|
};
|
|
1244
1301
|
try {
|
|
1245
|
-
|
|
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());
|
|
1246
1308
|
}
|
|
1247
1309
|
catch (error) {
|
|
1248
1310
|
if (isAssistantResponseTimeoutError(error)) {
|
|
@@ -1269,23 +1331,25 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1269
1331
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1270
1332
|
controllerPid: process.pid,
|
|
1271
1333
|
};
|
|
1272
|
-
throw new BrowserAutomationError(
|
|
1334
|
+
throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime }, error);
|
|
1273
1335
|
}
|
|
1274
1336
|
}
|
|
1275
1337
|
else {
|
|
1276
1338
|
throw error;
|
|
1277
1339
|
}
|
|
1278
1340
|
}
|
|
1279
|
-
const baselineNormalized = baselineAssistantText
|
|
1341
|
+
const baselineNormalized = baselineAssistantText
|
|
1342
|
+
? normalizeForComparison(baselineAssistantText)
|
|
1343
|
+
: "";
|
|
1280
1344
|
if (baselineNormalized) {
|
|
1281
|
-
const normalizedAnswer = normalizeForComparison(answer.text ??
|
|
1345
|
+
const normalizedAnswer = normalizeForComparison(answer.text ?? "");
|
|
1282
1346
|
const baselinePrefix = baselineNormalized.length >= 80
|
|
1283
1347
|
? baselineNormalized.slice(0, Math.min(200, baselineNormalized.length))
|
|
1284
|
-
:
|
|
1348
|
+
: "";
|
|
1285
1349
|
const isBaseline = normalizedAnswer === baselineNormalized ||
|
|
1286
1350
|
(baselinePrefix.length > 0 && normalizedAnswer.startsWith(baselinePrefix));
|
|
1287
1351
|
if (isBaseline) {
|
|
1288
|
-
logger(
|
|
1352
|
+
logger("Detected stale assistant response; waiting for new response...");
|
|
1289
1353
|
const refreshed = await waitForFreshAssistantResponse(baselineNormalized, 15_000);
|
|
1290
1354
|
if (refreshed) {
|
|
1291
1355
|
answer = refreshed;
|
|
@@ -1293,11 +1357,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1293
1357
|
}
|
|
1294
1358
|
}
|
|
1295
1359
|
answerText = answer.text;
|
|
1296
|
-
answerHtml = answer.html ??
|
|
1360
|
+
answerHtml = answer.html ?? "";
|
|
1297
1361
|
const copiedMarkdown = await withRetries(async () => {
|
|
1298
1362
|
const attempt = await captureAssistantMarkdown(Runtime, answer.meta, logger);
|
|
1299
1363
|
if (!attempt) {
|
|
1300
|
-
throw new Error(
|
|
1364
|
+
throw new Error("copy-missing");
|
|
1301
1365
|
}
|
|
1302
1366
|
return attempt;
|
|
1303
1367
|
}, {
|
|
@@ -1319,33 +1383,33 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1319
1383
|
allowMarkdownUpdate: !copiedMarkdown,
|
|
1320
1384
|
}));
|
|
1321
1385
|
// Final sanity check: ensure we didn't accidentally capture the user prompt instead of the assistant turn.
|
|
1322
|
-
const finalSnapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1323
|
-
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() : "";
|
|
1324
1388
|
if (finalText &&
|
|
1325
1389
|
finalText !== answerMarkdown.trim() &&
|
|
1326
1390
|
finalText !== promptText.trim() &&
|
|
1327
1391
|
finalText.length >= answerMarkdown.trim().length) {
|
|
1328
|
-
logger(
|
|
1392
|
+
logger("Refreshed assistant response via final DOM snapshot");
|
|
1329
1393
|
answerText = finalText;
|
|
1330
1394
|
answerMarkdown = finalText;
|
|
1331
1395
|
}
|
|
1332
1396
|
// Detect prompt echo using normalized comparison (whitespace-insensitive).
|
|
1333
1397
|
const promptEchoMatcher = buildPromptEchoMatcher(promptText);
|
|
1334
1398
|
const alignedEcho = alignPromptEchoPair(answerText, answerMarkdown, promptEchoMatcher, copiedMarkdown ? logger : undefined, {
|
|
1335
|
-
text:
|
|
1336
|
-
markdown:
|
|
1399
|
+
text: "Aligned assistant response text to copied markdown after prompt echo",
|
|
1400
|
+
markdown: "Aligned assistant markdown to response text after prompt echo",
|
|
1337
1401
|
});
|
|
1338
1402
|
answerText = alignedEcho.answerText;
|
|
1339
1403
|
answerMarkdown = alignedEcho.answerMarkdown;
|
|
1340
1404
|
const isPromptEcho = alignedEcho.isEcho;
|
|
1341
1405
|
if (isPromptEcho) {
|
|
1342
|
-
logger(
|
|
1406
|
+
logger("Detected prompt echo in response; waiting for actual assistant response...");
|
|
1343
1407
|
const deadline = Date.now() + 15_000;
|
|
1344
1408
|
let bestText = null;
|
|
1345
1409
|
let stableCount = 0;
|
|
1346
1410
|
while (Date.now() < deadline) {
|
|
1347
|
-
const snapshot = await readAssistantSnapshot(Runtime, baselineTurns ?? undefined).catch(() => null);
|
|
1348
|
-
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() : "";
|
|
1349
1413
|
const isStillEcho = !text || Boolean(promptEchoMatcher?.isEcho(text));
|
|
1350
1414
|
if (!isStillEcho) {
|
|
1351
1415
|
if (!bestText || text.length > bestText.length) {
|
|
@@ -1362,7 +1426,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1362
1426
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
1363
1427
|
}
|
|
1364
1428
|
if (bestText) {
|
|
1365
|
-
logger(
|
|
1429
|
+
logger("Recovered assistant response after detecting prompt echo");
|
|
1366
1430
|
answerText = bestText;
|
|
1367
1431
|
answerMarkdown = bestText;
|
|
1368
1432
|
}
|
|
@@ -1394,13 +1458,13 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1394
1458
|
connectionClosedUnexpectedly = connectionClosedUnexpectedly || socketClosed;
|
|
1395
1459
|
if (!socketClosed) {
|
|
1396
1460
|
logger(`Failed to complete ChatGPT run: ${normalizedError.message}`);
|
|
1397
|
-
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE ===
|
|
1461
|
+
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
1398
1462
|
logger(normalizedError.stack);
|
|
1399
1463
|
}
|
|
1400
1464
|
throw normalizedError;
|
|
1401
1465
|
}
|
|
1402
|
-
throw new BrowserAutomationError(
|
|
1403
|
-
stage:
|
|
1466
|
+
throw new BrowserAutomationError("Remote Chrome connection lost before Oracle finished.", {
|
|
1467
|
+
stage: "connection-lost",
|
|
1404
1468
|
runtime: {
|
|
1405
1469
|
chromeHost: host,
|
|
1406
1470
|
chromePort: port,
|
|
@@ -1426,20 +1490,20 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
1426
1490
|
logger(`Remote session complete • ${totalSeconds.toFixed(1)}s total`);
|
|
1427
1491
|
}
|
|
1428
1492
|
}
|
|
1429
|
-
export { estimateTokenCount } from
|
|
1430
|
-
export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from
|
|
1431
|
-
export { syncCookies } from
|
|
1432
|
-
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";
|
|
1433
1497
|
export async function maybeReuseRunningChromeForTest(userDataDir, logger, options = {}) {
|
|
1434
1498
|
return maybeReuseRunningChrome(userDataDir, logger, options);
|
|
1435
1499
|
}
|
|
1436
1500
|
export function isWebSocketClosureError(error) {
|
|
1437
1501
|
const message = error.message.toLowerCase();
|
|
1438
|
-
return (message.includes(
|
|
1439
|
-
message.includes(
|
|
1440
|
-
message.includes(
|
|
1441
|
-
message.includes(
|
|
1442
|
-
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"));
|
|
1443
1507
|
}
|
|
1444
1508
|
export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
1445
1509
|
const elapsedMs = now - startedAt;
|
|
@@ -1447,13 +1511,13 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
|
|
|
1447
1511
|
const progress = Math.min(1, elapsedMs / 600_000); // soft target: 10 minutes
|
|
1448
1512
|
const pct = Math.round(progress * 100)
|
|
1449
1513
|
.toString()
|
|
1450
|
-
.padStart(3,
|
|
1451
|
-
const statusLabel = message ? ` — ${message}` :
|
|
1514
|
+
.padStart(3, " ");
|
|
1515
|
+
const statusLabel = message ? ` — ${message}` : "";
|
|
1452
1516
|
return `${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
|
|
1453
1517
|
}
|
|
1454
|
-
async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, minTurnIndex) {
|
|
1518
|
+
async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger, minTurnIndex, expectedConversationId) {
|
|
1455
1519
|
try {
|
|
1456
|
-
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
1520
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
1457
1521
|
}
|
|
1458
1522
|
catch (error) {
|
|
1459
1523
|
if (!shouldReloadAfterAssistantError(error)) {
|
|
@@ -1463,20 +1527,20 @@ async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logg
|
|
|
1463
1527
|
if (!conversationUrl || !isConversationUrl(conversationUrl)) {
|
|
1464
1528
|
throw error;
|
|
1465
1529
|
}
|
|
1466
|
-
logger(
|
|
1530
|
+
logger("Assistant response stalled; reloading conversation and retrying once");
|
|
1467
1531
|
await Page.navigate({ url: conversationUrl });
|
|
1468
1532
|
await delay(1000);
|
|
1469
|
-
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex);
|
|
1533
|
+
return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
|
|
1470
1534
|
}
|
|
1471
1535
|
}
|
|
1472
1536
|
function shouldReloadAfterAssistantError(error) {
|
|
1473
1537
|
if (!(error instanceof Error))
|
|
1474
1538
|
return false;
|
|
1475
1539
|
const message = error.message.toLowerCase();
|
|
1476
|
-
return (message.includes(
|
|
1477
|
-
message.includes(
|
|
1478
|
-
message.includes(
|
|
1479
|
-
message.includes(
|
|
1540
|
+
return (message.includes("assistant-response") ||
|
|
1541
|
+
message.includes("watchdog") ||
|
|
1542
|
+
message.includes("timeout") ||
|
|
1543
|
+
message.includes("capture assistant response"));
|
|
1480
1544
|
}
|
|
1481
1545
|
function isAssistantResponseTimeoutError(error) {
|
|
1482
1546
|
if (!(error instanceof Error))
|
|
@@ -1484,15 +1548,15 @@ function isAssistantResponseTimeoutError(error) {
|
|
|
1484
1548
|
const message = error.message.toLowerCase();
|
|
1485
1549
|
if (!message)
|
|
1486
1550
|
return false;
|
|
1487
|
-
return (message.includes(
|
|
1488
|
-
message.includes(
|
|
1489
|
-
message.includes(
|
|
1490
|
-
message.includes(
|
|
1551
|
+
return (message.includes("assistant-response") ||
|
|
1552
|
+
message.includes("assistant response") ||
|
|
1553
|
+
message.includes("watchdog") ||
|
|
1554
|
+
message.includes("capture assistant response"));
|
|
1491
1555
|
}
|
|
1492
1556
|
async function readConversationUrl(Runtime) {
|
|
1493
1557
|
try {
|
|
1494
|
-
const currentUrl = await Runtime.evaluate({ expression:
|
|
1495
|
-
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;
|
|
1496
1560
|
}
|
|
1497
1561
|
catch {
|
|
1498
1562
|
return null;
|
|
@@ -1515,16 +1579,16 @@ async function validateChatGPTSession(Runtime, logger) {
|
|
|
1515
1579
|
});
|
|
1516
1580
|
const result = outcome.result?.value;
|
|
1517
1581
|
if (!result) {
|
|
1518
|
-
return { valid: false, reason:
|
|
1582
|
+
return { valid: false, reason: "Failed to evaluate session state" };
|
|
1519
1583
|
}
|
|
1520
1584
|
if (result.onAuthPage) {
|
|
1521
|
-
return { valid: false, reason:
|
|
1585
|
+
return { valid: false, reason: "Redirected to auth page" };
|
|
1522
1586
|
}
|
|
1523
1587
|
if (result.hasLoginCta) {
|
|
1524
|
-
return { valid: false, reason:
|
|
1588
|
+
return { valid: false, reason: "Login button detected on page" };
|
|
1525
1589
|
}
|
|
1526
1590
|
if (!result.hasTextarea) {
|
|
1527
|
-
return { valid: false, reason:
|
|
1591
|
+
return { valid: false, reason: "Prompt textarea not available" };
|
|
1528
1592
|
}
|
|
1529
1593
|
return { valid: true };
|
|
1530
1594
|
}
|
|
@@ -1611,9 +1675,9 @@ async function readConversationTurnCount(Runtime, logger) {
|
|
|
1611
1675
|
expression: `document.querySelectorAll(${selectorLiteral}).length`,
|
|
1612
1676
|
returnByValue: true,
|
|
1613
1677
|
});
|
|
1614
|
-
const raw = typeof result?.value ===
|
|
1678
|
+
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
1615
1679
|
if (!Number.isFinite(raw)) {
|
|
1616
|
-
throw new Error(
|
|
1680
|
+
throw new Error("Turn count not numeric");
|
|
1617
1681
|
}
|
|
1618
1682
|
return Math.max(0, Math.floor(raw));
|
|
1619
1683
|
}
|
|
@@ -1648,14 +1712,14 @@ function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false)
|
|
|
1648
1712
|
const nextMessage = await readThinkingStatus(Runtime);
|
|
1649
1713
|
if (nextMessage && nextMessage !== lastMessage) {
|
|
1650
1714
|
lastMessage = nextMessage;
|
|
1651
|
-
let locatorSuffix =
|
|
1715
|
+
let locatorSuffix = "";
|
|
1652
1716
|
if (includeDiagnostics) {
|
|
1653
1717
|
try {
|
|
1654
1718
|
const snapshot = await readAssistantSnapshot(Runtime);
|
|
1655
|
-
locatorSuffix = ` | assistant-turn=${snapshot ?
|
|
1719
|
+
locatorSuffix = ` | assistant-turn=${snapshot ? "present" : "missing"}`;
|
|
1656
1720
|
}
|
|
1657
1721
|
catch {
|
|
1658
|
-
locatorSuffix =
|
|
1722
|
+
locatorSuffix = " | assistant-turn=error";
|
|
1659
1723
|
}
|
|
1660
1724
|
}
|
|
1661
1725
|
logger(formatThinkingLog(startedAt, Date.now(), nextMessage, locatorSuffix));
|
|
@@ -1682,7 +1746,7 @@ async function readThinkingStatus(Runtime) {
|
|
|
1682
1746
|
const expression = buildThinkingStatusExpression();
|
|
1683
1747
|
try {
|
|
1684
1748
|
const { result } = await Runtime.evaluate({ expression, returnByValue: true });
|
|
1685
|
-
const value = typeof result.value ===
|
|
1749
|
+
const value = typeof result.value === "string" ? result.value.trim() : "";
|
|
1686
1750
|
const sanitized = sanitizeThinkingText(value);
|
|
1687
1751
|
return sanitized || null;
|
|
1688
1752
|
}
|
|
@@ -1692,12 +1756,12 @@ async function readThinkingStatus(Runtime) {
|
|
|
1692
1756
|
}
|
|
1693
1757
|
function sanitizeThinkingText(raw) {
|
|
1694
1758
|
if (!raw) {
|
|
1695
|
-
return
|
|
1759
|
+
return "";
|
|
1696
1760
|
}
|
|
1697
1761
|
const trimmed = raw.trim();
|
|
1698
1762
|
const prefixPattern = /^(pro thinking)\s*[•:\-–—]*\s*/i;
|
|
1699
1763
|
if (prefixPattern.test(trimmed)) {
|
|
1700
|
-
return trimmed.replace(prefixPattern,
|
|
1764
|
+
return trimmed.replace(prefixPattern, "").trim();
|
|
1701
1765
|
}
|
|
1702
1766
|
return trimmed;
|
|
1703
1767
|
}
|
|
@@ -1706,20 +1770,20 @@ function describeDevtoolsFirewallHint(host, port) {
|
|
|
1706
1770
|
return null;
|
|
1707
1771
|
return [
|
|
1708
1772
|
`DevTools port ${host}:${port} is blocked from WSL.`,
|
|
1709
|
-
|
|
1710
|
-
|
|
1773
|
+
"",
|
|
1774
|
+
"PowerShell (admin):",
|
|
1711
1775
|
`New-NetFirewallRule -DisplayName 'Chrome DevTools ${port}' -Direction Inbound -Action Allow -Protocol TCP -LocalPort ${port}`,
|
|
1712
1776
|
"New-NetFirewallRule -DisplayName 'Chrome DevTools (chrome.exe)' -Direction Inbound -Action Allow -Program 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -Protocol TCP",
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
].join(
|
|
1777
|
+
"",
|
|
1778
|
+
"Re-run the same oracle command after adding the rule.",
|
|
1779
|
+
].join("\n");
|
|
1716
1780
|
}
|
|
1717
1781
|
function isWsl() {
|
|
1718
|
-
if (process.platform !==
|
|
1782
|
+
if (process.platform !== "linux")
|
|
1719
1783
|
return false;
|
|
1720
1784
|
if (process.env.WSL_DISTRO_NAME)
|
|
1721
1785
|
return true;
|
|
1722
|
-
return os.release().toLowerCase().includes(
|
|
1786
|
+
return os.release().toLowerCase().includes("microsoft");
|
|
1723
1787
|
}
|
|
1724
1788
|
function extractConversationIdFromUrl(url) {
|
|
1725
1789
|
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
@@ -1729,9 +1793,9 @@ async function resolveUserDataBaseDir() {
|
|
|
1729
1793
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
1730
1794
|
if (isWsl()) {
|
|
1731
1795
|
const candidates = [
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1796
|
+
"/mnt/c/Users/Public/AppData/Local/Temp",
|
|
1797
|
+
"/mnt/c/Temp",
|
|
1798
|
+
"/mnt/c/Windows/Temp",
|
|
1735
1799
|
];
|
|
1736
1800
|
for (const candidate of candidates) {
|
|
1737
1801
|
try {
|
|
@@ -1743,18 +1807,49 @@ async function resolveUserDataBaseDir() {
|
|
|
1743
1807
|
}
|
|
1744
1808
|
}
|
|
1745
1809
|
}
|
|
1746
|
-
|
|
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);
|
|
1747
1834
|
}
|
|
1748
1835
|
function buildThinkingStatusExpression() {
|
|
1749
1836
|
const selectors = [
|
|
1750
|
-
|
|
1751
|
-
|
|
1837
|
+
"span.loading-shimmer",
|
|
1838
|
+
"span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary",
|
|
1752
1839
|
'[data-testid*="thinking"]',
|
|
1753
1840
|
'[data-testid*="reasoning"]',
|
|
1754
1841
|
'[role="status"]',
|
|
1755
1842
|
'[aria-live="polite"]',
|
|
1756
1843
|
];
|
|
1757
|
-
const keywords = [
|
|
1844
|
+
const keywords = [
|
|
1845
|
+
"pro thinking",
|
|
1846
|
+
"thinking",
|
|
1847
|
+
"reasoning",
|
|
1848
|
+
"clarifying",
|
|
1849
|
+
"planning",
|
|
1850
|
+
"drafting",
|
|
1851
|
+
"summarizing",
|
|
1852
|
+
];
|
|
1758
1853
|
const selectorLiteral = JSON.stringify(selectors);
|
|
1759
1854
|
const keywordsLiteral = JSON.stringify(keywords);
|
|
1760
1855
|
return `(() => {
|