@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.
Files changed (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -1,26 +1,86 @@
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, submitPrompt, 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
- export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from './constants.js';
19
- export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from './utils.js';
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('Prompt text is required when using browser mode.');
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 === '1') {
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('Note: --remote-chrome ignores local Chrome flags ' +
80
- '(--browser-headless, --browser-hide-window, --browser-keep-browser, --browser-chrome-path).');
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(), '.oracle', 'browser-profile');
147
+ : path.join(os.homedir(), ".oracle", "browser-profile");
88
148
  const userDataDir = manualLogin
89
149
  ? manualProfileDir
90
- : await mkdtemp(path.join(await resolveUserDataBaseDir(), 'oracle-browser-'));
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, { waitForPortMs: config.reuseChromeWaitMs })
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 ?? '127.0.0.1';
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 !== 'complete',
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 = 'attempted';
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, undefined, chromeHost, {
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('disconnect', () => {
220
+ client?.on("disconnect", () => {
158
221
  connectionClosedUnexpectedly = true;
159
- logger('Chrome window closed; attempting to abort run.');
160
- reject(new Error('Chrome window closed before oracle finished. Please keep it open until completion.'));
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 === 'function') {
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('Manual login mode: seeding persistent profile with cookies from your Chrome profile.');
244
+ logger("Manual login mode: seeding persistent profile with cookies from your Chrome profile.");
182
245
  }
183
246
  if (!config.inlineCookies) {
184
- logger('Heads-up: macOS may prompt for your Keychain password to read Chrome cookies; use --copy or --render for manual flow.');
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('Applying inline cookies (skipping Chrome profile read and Keychain prompt)');
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('No inline cookies were applied; aborting before navigation.');
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 ?? 'Default'}`
267
+ : `Copied ${cookieCount} cookies from Chrome profile ${config.chromeProfile ?? "Default"}`
205
268
  : config.inlineCookies
206
- ? 'No inline cookies applied; continuing without session reuse'
207
- : 'No Chrome cookies found; continuing without session reuse');
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
- ? 'Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in.'
212
- : 'Skipping Chrome cookie sync (--browser-no-cookie-sync)');
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('No ChatGPT cookies were applied from your Chrome profile; cannot proceed in browser mode. ' +
218
- 'Make sure ChatGPT is signed in in the selected profile, use --browser-manual-login / inline cookies, ' +
219
- 'or retry with --browser-cookie-wait 5s if Keychain prompts are slow.', {
220
- stage: 'execute-browser',
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 ?? 'Default',
285
+ profile: config.chromeProfile ?? "Default",
223
286
  cookiePath: config.chromeCookiePath ?? null,
224
- hint: 'If macOS Keychain prompts or denies access, run oracle from a GUI session or use --copy/--render for the manual flow.',
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({ runtime: Runtime, logger, appliedCookies, manualLogin, timeoutMs: config.timeoutMs }));
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: 'location.href',
330
+ expression: "location.href",
262
331
  returnByValue: true,
263
332
  });
264
- if (typeof result?.value === 'string') {
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({ expression: 'location.href', returnByValue: true });
294
- if (typeof result?.value === 'string' && result.value.includes('/c/')) {
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 !== 'ignore') {
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
- ? ' No cookies were applied; log in to ChatGPT in Chrome or provide inline cookies (--browser-inline-cookies[(-file)] or ORACLE_BROWSER_COOKIES_JSON).'
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 === 'ignore') {
342
- logger('Model picker: skipped (strategy=ignore)');
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 === 'string' ? baselineSnapshot.text.trim() : '';
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('Chrome DOM domain unavailable while uploading attachments.');
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
- try {
399
- await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
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 sendAttachmentNames = attachmentWaitTimedOut ? [] : attachmentNames;
416
- const committedTurns = await submitPrompt({
474
+ const providerState = {
417
475
  runtime: Runtime,
418
476
  input: Input,
419
- attachmentNames: sendAttachmentNames,
420
- baselineTurns: baselineTurns ?? undefined,
477
+ logger,
478
+ timeoutMs: config.timeoutMs,
421
479
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
422
- }, prompt, logger);
423
- if (typeof committedTurns === 'number' && Number.isFinite(committedTurns)) {
424
- if (baselineTurns === null || committedTurns > baselineTurns) {
425
- baselineTurns = Math.max(0, committedTurns - 1);
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 (attachmentWaitTimedOut) {
430
- logger('Attachment confirmation timed out; skipping user-turn attachment verification.');
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
- throw new Error('Sent user message did not expose attachment UI after upload.');
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('post-submit', config.timeoutMs ?? 120_000);
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
- try {
452
- const submission = await raceWithDisconnect(submitOnce(promptText, attachments));
453
- baselineTurns = submission.baselineTurns;
454
- baselineAssistantText = submission.baselineAssistantText;
455
- }
456
- catch (error) {
457
- const isPromptTooLarge = error instanceof BrowserAutomationError &&
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
- const submission = await raceWithDisconnect(submitOnce(fallbackSubmission.prompt, fallbackSubmission.attachments));
465
- baselineTurns = submission.baselineTurns;
466
- baselineAssistantText = submission.baselineAssistantText;
467
- }
468
- else {
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, ' ').trim();
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 === 'string' ? snapshot.text.trim() : '';
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 || (baselinePrefix.length > 0 && normalized.startsWith(baselinePrefix));
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: { turnId: snapshot?.turnId ?? undefined, messageId: snapshot?.messageId ?? undefined },
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('assistant-recheck', 15_000).catch(() => false);
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 || 'unknown'}. ` +
597
+ `Conversation URL: ${conversationUrl || lastUrl || "unknown"}. ` +
526
598
  `Please sign in and retry.`, {
527
- stage: 'assistant-recheck',
599
+ stage: "assistant-recheck",
528
600
  details: {
529
601
  conversationUrl: conversationUrl || lastUrl || null,
530
- sessionStatus: 'needs_login',
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('Recovered assistant response after delayed recheck');
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
- answer = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined));
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('assistant-timeout', 15_000).catch(() => false);
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('Assistant response timed out before completion; reattach later to capture the answer.', { stage: 'assistant-timeout', runtime }, error);
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('post-response', 15_000);
581
- const baselineNormalized = baselineAssistantText ? normalizeForComparison(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('Detected stale assistant response; waiting for new response...');
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('copy-missing');
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 === 'string' ? finalSnapshot.text.trim() : '';
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('Refreshed assistant response via final DOM snapshot');
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: 'Aligned assistant response text to copied markdown after prompt echo',
644
- markdown: 'Aligned assistant markdown to response text after prompt echo',
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('Detected prompt echo in response; waiting for actual assistant response...');
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 === 'string' ? snapshot.text.trim() : '';
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('Recovered assistant response after detecting prompt echo');
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 === 'string' ? snapshot.text.trim() : '';
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('Refreshed short assistant response from latest DOM snapshot');
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('Chrome disconnected before completion');
781
+ throw new Error("Chrome disconnected before completion");
707
782
  }
708
783
  stopThinkingMonitor?.();
709
- runStatus = 'complete';
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 === '1') && normalizedError.stack) {
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 === '1') && normalizedError.stack) {
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('Chrome window closed before oracle finished. Please keep it open until completion.', {
747
- stage: 'connection-lost',
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 === 'complete' && isolatedTargetId && chrome?.port) {
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
- if (!effectiveKeepBrowser) {
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: 'never' }).catch(() => undefined);
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('error', () => resolve(false));
825
- server.once('listening', () => {
922
+ server.once("error", () => resolve(false));
923
+ server.once("listening", () => {
826
924
  server.close(() => resolve(true));
827
925
  });
828
- server.listen(port, '127.0.0.1');
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('error', (error) => {
932
+ server.once("error", (error) => {
835
933
  server.close();
836
934
  reject(error);
837
935
  });
838
- server.listen(0, '127.0.0.1', () => {
936
+ server.listen(0, "127.0.0.1", () => {
839
937
  const address = server.address();
840
- if (address && typeof address === 'object') {
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('Failed to acquire ephemeral port')));
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('login button');
865
- const sessionMissing = message?.toLowerCase().includes('session not detected');
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('Manual login mode: please sign into chatgpt.com in the opened Chrome window; waiting for session to appear...');
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('Manual login mode timed out waiting for ChatGPT session; please sign in and retry.');
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 === 'string' ? laterSnapshot.text.trim() : '';
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 === 'string' ? 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('ChatGPT session not detected; page never left new tab.', {
926
- stage: 'execute-browser',
927
- details: { url: lastUrl || '(empty)' },
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: 'if_oracle_pid_dead' });
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('Remote Chrome configuration missing. Pass --remote-chrome <host:port> to use this mode.');
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('disconnect', markConnectionLost);
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 === 'function') {
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('Skipping cookie sync for remote Chrome (using existing session)');
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: 'location.href',
1117
+ expression: "location.href",
1020
1118
  returnByValue: true,
1021
1119
  });
1022
- if (typeof result?.value === 'string') {
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 !== 'ignore') {
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 === 'ignore') {
1045
- logger('Model picker: skipped (strategy=ignore)');
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 === 'string' ? baselineSnapshot.text.trim() : '';
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('Chrome DOM domain unavailable while uploading attachments.');
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('All attachments uploaded');
1178
+ logger("All attachments uploaded");
1081
1179
  }
1082
1180
  let baselineTurns = await readConversationTurnCount(Runtime, logger);
1083
- const committedTurns = await submitPrompt({
1181
+ const providerState = {
1084
1182
  runtime: Runtime,
1085
1183
  input: Input,
1086
- attachmentNames,
1087
- baselineTurns: baselineTurns ?? undefined,
1184
+ logger,
1185
+ timeoutMs: config.timeoutMs,
1088
1186
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
1089
- }, prompt, logger);
1090
- if (typeof committedTurns === 'number' && Number.isFinite(committedTurns)) {
1091
- if (baselineTurns === null || committedTurns > baselineTurns) {
1092
- baselineTurns = Math.max(0, committedTurns - 1);
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
- try {
1100
- const submission = await submitOnce(promptText, attachments);
1101
- baselineTurns = submission.baselineTurns;
1102
- baselineAssistantText = submission.baselineAssistantText;
1103
- }
1104
- catch (error) {
1105
- const isPromptTooLarge = error instanceof BrowserAutomationError &&
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
- const submission = await submitOnce(options.fallbackSubmission.prompt, options.fallbackSubmission.attachments);
1112
- baselineTurns = submission.baselineTurns;
1113
- baselineAssistantText = submission.baselineAssistantText;
1114
- }
1115
- else {
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, ' ').trim();
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 === 'string' ? snapshot.text.trim() : '';
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 || (baselinePrefix.length > 0 && normalized.startsWith(baselinePrefix));
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: { turnId: snapshot?.turnId ?? undefined, messageId: snapshot?.messageId ?? undefined },
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 || 'unknown'}. ` +
1277
+ `Conversation URL: ${conversationUrl || lastUrl || "unknown"}. ` +
1168
1278
  `Please sign in and retry.`, {
1169
- stage: 'assistant-recheck',
1279
+ stage: "assistant-recheck",
1170
1280
  details: {
1171
1281
  conversationUrl: conversationUrl || lastUrl || null,
1172
- sessionStatus: 'needs_login',
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('Recovered assistant response after delayed recheck');
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
- answer = await waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined);
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('Assistant response timed out before completion; reattach later to capture the answer.', { stage: 'assistant-timeout', runtime }, error);
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 ? normalizeForComparison(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('Detected stale assistant response; waiting for new response...');
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('copy-missing');
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 === 'string' ? finalSnapshot.text.trim() : '';
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('Refreshed assistant response via final DOM snapshot');
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: 'Aligned assistant response text to copied markdown after prompt echo',
1283
- markdown: 'Aligned assistant markdown to response text after prompt echo',
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('Detected prompt echo in response; waiting for actual assistant response...');
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 === 'string' ? snapshot.text.trim() : '';
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('Recovered assistant response after detecting prompt echo');
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 === '1') && normalizedError.stack) {
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('Remote Chrome connection lost before Oracle finished.', {
1350
- stage: 'connection-lost',
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 './utils.js';
1377
- export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from './config.js';
1378
- export { syncCookies } from './cookies.js';
1379
- export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from './pageActions.js';
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('websocket connection closed') ||
1386
- message.includes('websocket is closed') ||
1387
- message.includes('websocket error') ||
1388
- message.includes('target closed'));
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('Assistant response stalled; reloading conversation and retrying once');
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('assistant-response') ||
1423
- message.includes('watchdog') ||
1424
- message.includes('timeout') ||
1425
- message.includes('capture assistant response'));
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('assistant-response') ||
1434
- message.includes('assistant response') ||
1435
- message.includes('watchdog') ||
1436
- message.includes('capture assistant response'));
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: 'location.href', returnByValue: true });
1441
- return typeof currentUrl.result?.value === 'string' ? currentUrl.result.value : null;
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: 'Failed to evaluate session state' };
1582
+ return { valid: false, reason: "Failed to evaluate session state" };
1465
1583
  }
1466
1584
  if (result.onAuthPage) {
1467
- return { valid: false, reason: 'Redirected to auth page' };
1585
+ return { valid: false, reason: "Redirected to auth page" };
1468
1586
  }
1469
1587
  if (result.hasLoginCta) {
1470
- return { valid: false, reason: 'Login button detected on page' };
1588
+ return { valid: false, reason: "Login button detected on page" };
1471
1589
  }
1472
1590
  if (!result.hasTextarea) {
1473
- return { valid: false, reason: 'Prompt textarea not available' };
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 === 'number' ? result.value : Number(result?.value);
1678
+ const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
1561
1679
  if (!Number.isFinite(raw)) {
1562
- throw new Error('Turn count not numeric');
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 ? 'present' : 'missing'}`;
1719
+ locatorSuffix = ` | assistant-turn=${snapshot ? "present" : "missing"}`;
1602
1720
  }
1603
1721
  catch {
1604
- locatorSuffix = ' | assistant-turn=error';
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 === 'string' ? result.value.trim() : '';
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, '').trim();
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
- 'PowerShell (admin):',
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
- 'Re-run the same oracle command after adding the rule.',
1661
- ].join('\n');
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 !== 'linux')
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('microsoft');
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
- '/mnt/c/Users/Public/AppData/Local/Temp',
1679
- '/mnt/c/Temp',
1680
- '/mnt/c/Windows/Temp',
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
- return os.tmpdir();
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
- 'span.loading-shimmer',
1697
- 'span.flex.items-center.gap-1.truncate.text-start.align-middle.text-token-text-tertiary',
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 = ['pro thinking', 'thinking', 'reasoning', 'clarifying', 'planning', 'drafting', 'summarizing'];
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 `(() => {