@steipete/oracle 0.9.0 → 0.10.0

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