@steipete/oracle 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,12 @@ Engine auto-picks API when `OPENAI_API_KEY` is set, otherwise browser; browser i
64
64
  ```
65
65
  - Tip: set `browser.chatgptUrl` in config (or `--chatgpt-url`) to a dedicated ChatGPT project folder so browser runs don’t clutter your main history.
66
66
 
67
+ **Codex skill**
68
+ - Copy the bundled skill from this repo to your Codex skills folder:
69
+ - `mkdir -p ~/.codex/skills`
70
+ - `cp -R skills/oracle ~/.codex/skills/oracle`
71
+ - Then reference it in your `AGENTS.md`/`CLAUDE.md` so Codex loads it.
72
+
67
73
  **MCP**
68
74
  - Run the stdio server via `oracle-mcp`.
69
75
  - Configure clients via [steipete/mcporter](https://github.com/steipete/mcporter) or `.mcp.json`; see [docs/mcp.md](docs/mcp.md) for connection examples.
@@ -174,7 +174,9 @@ program
174
174
  .addOption(new Option('--browser-headless', 'Launch Chrome in headless mode.').hideHelp())
175
175
  .addOption(new Option('--browser-hide-window', 'Hide the Chrome window after launch (macOS headful only).').hideHelp())
176
176
  .addOption(new Option('--browser-keep-browser', 'Keep Chrome running after completion.').hideHelp())
177
- .addOption(new Option('--browser-extended-thinking', 'Select Extended thinking time for GPT-5.2 Thinking model.').hideHelp())
177
+ .addOption(new Option('--browser-thinking-time <level>', 'Thinking time intensity for Thinking/Pro models: light, standard, extended, heavy.')
178
+ .choices(['light', 'standard', 'extended', 'heavy'])
179
+ .hideHelp())
178
180
  .addOption(new Option('--browser-allow-cookie-errors', 'Continue even if Chrome cookies cannot be copied.').hideHelp())
179
181
  .addOption(new Option('--browser-attachments <mode>', 'How to deliver --file inputs in browser mode: auto (default) pastes inline up to ~60k chars then uploads; never always paste inline; always always upload.')
180
182
  .choices(['auto', 'never', 'always'])
@@ -177,12 +177,15 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
177
177
  }
178
178
  await dom.setFileInputFiles({ nodeId: resultNode.nodeId, files: [attachment.path] });
179
179
  await runtime.evaluate({ expression: dispatchEventsFor(idx), returnByValue: true }).catch(() => undefined);
180
- const probeDeadline = Date.now() + 4000;
180
+ await delay(350);
181
+ const probeDeadline = Date.now() + 6500;
182
+ const pokeIntervalMs = 1200;
181
183
  let lastPoke = 0;
184
+ let seenInputHasFile = false;
182
185
  while (Date.now() < probeDeadline) {
183
186
  // ChatGPT's composer can take a moment to hydrate the file-input onChange handler after navigation/model switches.
184
187
  // If the UI hasn't reacted yet, poke the input a few times to ensure the handler fires once it's mounted.
185
- if (Date.now() - lastPoke > 650) {
188
+ if (!seenInputHasFile && Date.now() - lastPoke > pokeIntervalMs) {
186
189
  lastPoke = Date.now();
187
190
  await runtime.evaluate({ expression: dispatchEventsFor(idx), returnByValue: true }).catch(() => undefined);
188
191
  }
@@ -198,6 +201,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
198
201
  composerText: typeof snapshot.composerText === 'string' ? snapshot.composerText : '',
199
202
  };
200
203
  const inputHasFile = finalSnapshot.inputNames.some((name) => name.toLowerCase().includes(expectedName.toLowerCase()));
204
+ seenInputHasFile = seenInputHasFile || inputHasFile;
201
205
  const expectedLower = expectedName.toLowerCase();
202
206
  const expectedNoExt = expectedLower.replace(/\.[a-z0-9]{1,10}$/i, '');
203
207
  const uiAcknowledged = finalSnapshot.chipCount > baselineChipCount ||
@@ -209,7 +213,7 @@ export async function uploadAttachmentFile(deps, attachment, logger) {
209
213
  break;
210
214
  }
211
215
  }
212
- await delay(200);
216
+ await delay(250);
213
217
  }
214
218
  const inputHasFile = finalSnapshot?.inputNames?.some((name) => name.toLowerCase().includes(expectedName.toLowerCase())) ?? false;
215
219
  const uiAcknowledged = (finalSnapshot?.chipCount ?? 0) > baselineChipCount;
@@ -97,6 +97,10 @@ function buildModelSelectionExpression(targetModel) {
97
97
  if (wantsPro && !normalizedLabel.includes(' pro')) return false;
98
98
  if (wantsInstant && !normalizedLabel.includes('instant')) return false;
99
99
  if (wantsThinking && !normalizedLabel.includes('thinking')) return false;
100
+ // Also reject if button has variants we DON'T want
101
+ if (!wantsPro && normalizedLabel.includes(' pro')) return false;
102
+ if (!wantsInstant && normalizedLabel.includes('instant')) return false;
103
+ if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
100
104
  return true;
101
105
  };
102
106
 
@@ -172,14 +176,21 @@ function buildModelSelectionExpression(targetModel) {
172
176
  return 0;
173
177
  }
174
178
  }
175
- const matches = TEST_IDS.filter((id) => id && normalizedTestId.includes(id));
176
- if (matches.length > 0) {
177
- // Prefer the most specific match (longest token) instead of treating any hit as equal.
178
- // This prevents generic tokens (e.g. "pro") from outweighing version-specific targets.
179
- const best = matches.reduce((acc, token) => (token.length > acc.length ? token : acc), '');
180
- score += 200 + Math.min(900, best.length * 25);
181
- if (best.startsWith('model-switcher-')) score += 120;
182
- if (best.includes('gpt-')) score += 60;
179
+ // Exact testid matches take priority over substring matches
180
+ const exactMatch = TEST_IDS.find((id) => id && normalizedTestId === id);
181
+ if (exactMatch) {
182
+ score += 1500;
183
+ if (exactMatch.startsWith('model-switcher-')) score += 200;
184
+ } else {
185
+ const matches = TEST_IDS.filter((id) => id && normalizedTestId.includes(id));
186
+ if (matches.length > 0) {
187
+ // Prefer the most specific match (longest token) instead of treating any hit as equal.
188
+ // This prevents generic tokens (e.g. "pro") from outweighing version-specific targets.
189
+ const best = matches.reduce((acc, token) => (token.length > acc.length ? token : acc), '');
190
+ score += 200 + Math.min(900, best.length * 25);
191
+ if (best.startsWith('model-switcher-')) score += 120;
192
+ if (best.includes('gpt-')) score += 60;
193
+ }
183
194
  }
184
195
  }
185
196
  if (normalizedText && normalizedTarget) {
@@ -215,6 +226,22 @@ function buildModelSelectionExpression(targetModel) {
215
226
  } else if (normalizedText.includes(' pro')) {
216
227
  score -= 40;
217
228
  }
229
+ // Similarly for Thinking variant
230
+ if (wantsThinking) {
231
+ if (!normalizedText.includes('thinking') && !normalizedTestId.includes('thinking')) {
232
+ score -= 80;
233
+ }
234
+ } else if (normalizedText.includes('thinking') || normalizedTestId.includes('thinking')) {
235
+ score -= 40;
236
+ }
237
+ // Similarly for Instant variant
238
+ if (wantsInstant) {
239
+ if (!normalizedText.includes('instant') && !normalizedTestId.includes('instant')) {
240
+ score -= 80;
241
+ }
242
+ } else if (normalizedText.includes('instant') || normalizedTestId.includes('instant')) {
243
+ score -= 40;
244
+ }
218
245
  return Math.max(score, 0);
219
246
  };
220
247
 
@@ -377,10 +404,24 @@ function buildModelMatchersLiteral(targetModel) {
377
404
  push('gpt5-2', labelTokens);
378
405
  push('gpt52', labelTokens);
379
406
  push('chatgpt 5.2', labelTokens);
380
- if (base.includes('thinking'))
407
+ // Thinking variant: explicit testid for "Thinking" picker option
408
+ if (base.includes('thinking')) {
381
409
  push('thinking', labelTokens);
382
- if (base.includes('instant'))
410
+ testIdTokens.add('model-switcher-gpt-5-2-thinking');
411
+ testIdTokens.add('gpt-5-2-thinking');
412
+ testIdTokens.add('gpt-5.2-thinking');
413
+ }
414
+ // Instant variant: explicit testid for "Instant" picker option
415
+ if (base.includes('instant')) {
383
416
  push('instant', labelTokens);
417
+ testIdTokens.add('model-switcher-gpt-5-2-instant');
418
+ testIdTokens.add('gpt-5-2-instant');
419
+ testIdTokens.add('gpt-5.2-instant');
420
+ }
421
+ // Base 5.2 testids (for "Auto" mode when no suffix specified)
422
+ if (!base.includes('thinking') && !base.includes('instant') && !base.includes('pro')) {
423
+ testIdTokens.add('model-switcher-gpt-5-2');
424
+ }
384
425
  testIdTokens.add('gpt-5-2');
385
426
  testIdTokens.add('gpt5-2');
386
427
  testIdTokens.add('gpt52');
@@ -1,14 +1,19 @@
1
1
  import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR } from '../constants.js';
2
2
  import { logDomFailure } from '../domDebug.js';
3
3
  import { buildClickDispatcher } from './domEvents.js';
4
- export async function ensureExtendedThinking(Runtime, logger) {
5
- const result = await evaluateThinkingTimeSelection(Runtime);
4
+ /**
5
+ * Selects a specific thinking time level in ChatGPT's composer pill menu.
6
+ * @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
7
+ */
8
+ export async function ensureThinkingTime(Runtime, level, logger) {
9
+ const result = await evaluateThinkingTimeSelection(Runtime, level);
10
+ const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
6
11
  switch (result?.status) {
7
- case 'already-extended':
8
- logger(`Thinking time: ${result.label ?? 'Extended'} (already selected)`);
12
+ case 'already-selected':
13
+ logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
9
14
  return;
10
15
  case 'switched':
11
- logger(`Thinking time: ${result.label ?? 'Extended'}`);
16
+ logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
12
17
  return;
13
18
  case 'chip-not-found': {
14
19
  await logDomFailure(Runtime, logger, 'thinking-chip');
@@ -18,33 +23,35 @@ export async function ensureExtendedThinking(Runtime, logger) {
18
23
  await logDomFailure(Runtime, logger, 'thinking-time-menu');
19
24
  throw new Error('Unable to find the Thinking time dropdown menu.');
20
25
  }
21
- case 'extended-not-found': {
22
- await logDomFailure(Runtime, logger, 'extended-option');
23
- throw new Error('Unable to find the Extended option in the Thinking time menu.');
26
+ case 'option-not-found': {
27
+ await logDomFailure(Runtime, logger, `${level}-option`);
28
+ throw new Error(`Unable to find the ${capitalizedLevel} option in the Thinking time menu.`);
24
29
  }
25
30
  default: {
26
31
  await logDomFailure(Runtime, logger, 'thinking-time-unknown');
27
- throw new Error('Unknown error selecting Extended thinking time.');
32
+ throw new Error(`Unknown error selecting ${capitalizedLevel} thinking time.`);
28
33
  }
29
34
  }
30
35
  }
31
36
  /**
32
- * Best-effort selection of the "Extended" thinking-time option in ChatGPT's composer pill menu.
37
+ * Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
33
38
  * Safe by default: if the pill/menu/option isn't present, we continue without throwing.
39
+ * @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
34
40
  */
35
- export async function ensureExtendedThinkingIfAvailable(Runtime, logger) {
41
+ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
36
42
  try {
37
- const result = await evaluateThinkingTimeSelection(Runtime);
43
+ const result = await evaluateThinkingTimeSelection(Runtime, level);
44
+ const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
38
45
  switch (result?.status) {
39
- case 'already-extended':
40
- logger(`Thinking time: ${result.label ?? 'Extended'} (already selected)`);
46
+ case 'already-selected':
47
+ logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
41
48
  return true;
42
49
  case 'switched':
43
- logger(`Thinking time: ${result.label ?? 'Extended'}`);
50
+ logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
44
51
  return true;
45
52
  case 'chip-not-found':
46
53
  case 'menu-not-found':
47
- case 'extended-not-found':
54
+ case 'option-not-found':
48
55
  if (logger.verbose) {
49
56
  logger(`Thinking time: ${result.status.replaceAll('-', ' ')}; continuing with default.`);
50
57
  }
@@ -65,22 +72,24 @@ export async function ensureExtendedThinkingIfAvailable(Runtime, logger) {
65
72
  return false;
66
73
  }
67
74
  }
68
- async function evaluateThinkingTimeSelection(Runtime) {
75
+ async function evaluateThinkingTimeSelection(Runtime, level) {
69
76
  const outcome = await Runtime.evaluate({
70
- expression: buildThinkingTimeExpression(),
77
+ expression: buildThinkingTimeExpression(level),
71
78
  awaitPromise: true,
72
79
  returnByValue: true,
73
80
  });
74
81
  return outcome.result?.value;
75
82
  }
76
- function buildThinkingTimeExpression() {
83
+ function buildThinkingTimeExpression(level) {
77
84
  const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
78
85
  const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
86
+ const targetLevelLiteral = JSON.stringify(level.toLowerCase());
79
87
  return `(async () => {
80
88
  ${buildClickDispatcher()}
81
89
 
82
90
  const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
83
91
  const MENU_ITEM_SELECTOR = ${menuItemLiteral};
92
+ const TARGET_LEVEL = ${targetLevelLiteral};
84
93
 
85
94
  const CHIP_SELECTORS = [
86
95
  '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
@@ -136,11 +145,11 @@ function buildThinkingTimeExpression() {
136
145
  return null;
137
146
  };
138
147
 
139
- const findExtendedOption = (menu) => {
148
+ const findTargetOption = (menu) => {
140
149
  const items = menu.querySelectorAll(MENU_ITEM_SELECTOR);
141
150
  for (const item of items) {
142
151
  const text = normalize(item.textContent ?? '');
143
- if (text.includes('extended')) {
152
+ if (text.includes(TARGET_LEVEL)) {
144
153
  return item;
145
154
  }
146
155
  }
@@ -167,24 +176,24 @@ function buildThinkingTimeExpression() {
167
176
  return;
168
177
  }
169
178
 
170
- const extendedOption = findExtendedOption(menu);
171
- if (!extendedOption) {
172
- resolve({ status: 'extended-not-found' });
179
+ const targetOption = findTargetOption(menu);
180
+ if (!targetOption) {
181
+ resolve({ status: 'option-not-found' });
173
182
  return;
174
183
  }
175
184
 
176
185
  const alreadySelected =
177
- optionIsSelected(extendedOption) ||
178
- optionIsSelected(extendedOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
179
- const label = extendedOption.textContent?.trim?.() || null;
180
- dispatchClickSequence(extendedOption);
181
- resolve({ status: alreadySelected ? 'already-extended' : 'switched', label });
186
+ optionIsSelected(targetOption) ||
187
+ optionIsSelected(targetOption.querySelector?.('[aria-checked="true"], [data-state="checked"], [data-state="selected"]'));
188
+ const label = targetOption.textContent?.trim?.() || null;
189
+ dispatchClickSequence(targetOption);
190
+ resolve({ status: alreadySelected ? 'already-selected' : 'switched', label });
182
191
  };
183
192
 
184
193
  setTimeout(attempt, INITIAL_WAIT_MS);
185
194
  });
186
195
  })()`;
187
196
  }
188
- export function buildThinkingTimeExpressionForTest() {
189
- return buildThinkingTimeExpression();
197
+ export function buildThinkingTimeExpressionForTest(level = 'extended') {
198
+ return buildThinkingTimeExpression(level);
190
199
  }
@@ -25,6 +25,7 @@ export async function launchChrome(config, userDataDir, logger) {
25
25
  chromePath: config.chromePath ?? undefined,
26
26
  chromeFlags,
27
27
  userDataDir,
28
+ handleSIGINT: false,
28
29
  port: debugPort ?? undefined,
29
30
  });
30
31
  const pidLabel = typeof launcher.pid === 'number' ? ` (pid ${launcher.pid})` : '';
@@ -216,6 +217,7 @@ async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host
216
217
  chromePath: chromePath ?? undefined,
217
218
  chromeFlags,
218
219
  userDataDir,
220
+ handleSIGINT: false,
219
221
  port: requestedPort ?? undefined,
220
222
  });
221
223
  if (host) {
@@ -24,7 +24,6 @@ export const DEFAULT_BROWSER_CONFIG = {
24
24
  remoteChrome: null,
25
25
  manualLogin: false,
26
26
  manualLoginProfileDir: null,
27
- extendedThinking: false,
28
27
  };
29
28
  export function resolveBrowserConfig(config) {
30
29
  const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
@@ -64,6 +63,7 @@ export function resolveBrowserConfig(config) {
64
63
  chromeCookiePath: config?.chromeCookiePath ?? DEFAULT_BROWSER_CONFIG.chromeCookiePath,
65
64
  debug: config?.debug ?? DEFAULT_BROWSER_CONFIG.debug,
66
65
  allowCookieErrors: config?.allowCookieErrors ?? envAllowCookieErrors ?? DEFAULT_BROWSER_CONFIG.allowCookieErrors,
66
+ thinkingTime: config?.thinkingTime,
67
67
  manualLogin,
68
68
  manualLoginProfileDir: manualLogin ? resolvedProfileDir : null,
69
69
  };
@@ -7,7 +7,7 @@ import { launchChrome, registerTerminationHooks, hideChromeWindow, connectToChro
7
7
  import { syncCookies } from './cookies.js';
8
8
  import { navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureModelSelection, submitPrompt, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from './pageActions.js';
9
9
  import { uploadAttachmentViaDataTransfer } from './actions/remoteFileTransfer.js';
10
- import { ensureExtendedThinking } from './actions/thinkingTime.js';
10
+ import { ensureThinkingTime } from './actions/thinkingTime.js';
11
11
  import { estimateTokenCount, withRetries, delay } from './utils.js';
12
12
  import { formatElapsed } from '../oracle/format.js';
13
13
  import { CHATGPT_URL } from './constants.js';
@@ -37,12 +37,14 @@ export async function runBrowserMode(options) {
37
37
  if (!runtimeHintCb || !chrome?.port) {
38
38
  return;
39
39
  }
40
+ const conversationId = lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined;
40
41
  const hint = {
41
42
  chromePid: chrome.pid,
42
43
  chromePort: chrome.port,
43
44
  chromeHost,
44
45
  chromeTargetId: lastTargetId,
45
46
  tabUrl: lastUrl,
47
+ conversationId,
46
48
  userDataDir,
47
49
  controllerPid: process.pid,
48
50
  };
@@ -264,13 +266,15 @@ export async function runBrowserMode(options) {
264
266
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
265
267
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
266
268
  }
267
- if (config.extendedThinking) {
268
- await raceWithDisconnect(withRetries(() => ensureExtendedThinking(Runtime, logger), {
269
+ // Handle thinking time selection if specified
270
+ const thinkingTime = config.thinkingTime;
271
+ if (thinkingTime) {
272
+ await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
269
273
  retries: 2,
270
274
  delayMs: 300,
271
275
  onRetry: (attempt, error) => {
272
276
  if (options.verbose) {
273
- logger(`[retry] Extended thinking attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
277
+ logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
274
278
  }
275
279
  },
276
280
  }));
@@ -284,6 +288,7 @@ export async function runBrowserMode(options) {
284
288
  for (const attachment of submissionAttachments) {
285
289
  logger(`Uploading attachment: ${attachment.displayPath}`);
286
290
  await uploadAttachmentFile({ runtime: Runtime, dom: DOM }, attachment, logger);
291
+ await delay(500);
287
292
  }
288
293
  // Scale timeout based on number of files: base 30s + 15s per additional file
289
294
  const baseTimeout = config.inputTimeoutMs ?? 30_000;
@@ -315,7 +320,7 @@ export async function runBrowserMode(options) {
315
320
  }
316
321
  }
317
322
  stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
318
- const answer = await raceWithDisconnect(waitForAssistantResponse(Runtime, config.timeoutMs, logger));
323
+ const answer = await raceWithDisconnect(waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger));
319
324
  answerText = answer.text;
320
325
  answerHtml = answer.html ?? '';
321
326
  const copiedMarkdown = await raceWithDisconnect(withRetries(async () => {
@@ -661,13 +666,15 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
661
666
  await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
662
667
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
663
668
  }
664
- if (config.extendedThinking) {
665
- await withRetries(() => ensureExtendedThinking(Runtime, logger), {
669
+ // Handle thinking time selection if specified
670
+ const thinkingTime = config.thinkingTime;
671
+ if (thinkingTime) {
672
+ await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
666
673
  retries: 2,
667
674
  delayMs: 300,
668
675
  onRetry: (attempt, error) => {
669
676
  if (options.verbose) {
670
- logger(`[retry] Extended thinking attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
677
+ logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
671
678
  }
672
679
  },
673
680
  });
@@ -682,6 +689,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
682
689
  for (const attachment of submissionAttachments) {
683
690
  logger(`Uploading attachment: ${attachment.displayPath}`);
684
691
  await uploadAttachmentViaDataTransfer({ runtime: Runtime, dom: DOM }, attachment, logger);
692
+ await delay(500);
685
693
  }
686
694
  // Scale timeout based on number of files: base 30s + 15s per additional file
687
695
  const baseTimeout = config.inputTimeoutMs ?? 30_000;
@@ -709,7 +717,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
709
717
  }
710
718
  }
711
719
  stopThinkingMonitor = startThinkingStatusMonitor(Runtime, logger, options.verbose ?? false);
712
- const answer = await waitForAssistantResponse(Runtime, config.timeoutMs, logger);
720
+ const answer = await waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger);
713
721
  answerText = answer.text;
714
722
  answerHtml = answer.html ?? '';
715
723
  const copiedMarkdown = await withRetries(async () => {
@@ -859,6 +867,42 @@ export function formatThinkingLog(startedAt, now, message, locatorSuffix) {
859
867
  const statusLabel = message ? ` — ${message}` : '';
860
868
  return `${pct}% [${elapsedText} / ~10m]${statusLabel}${locatorSuffix}`;
861
869
  }
870
+ async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logger) {
871
+ try {
872
+ return await waitForAssistantResponse(Runtime, timeoutMs, logger);
873
+ }
874
+ catch (error) {
875
+ if (!shouldReloadAfterAssistantError(error)) {
876
+ throw error;
877
+ }
878
+ const conversationUrl = await readConversationUrl(Runtime);
879
+ if (!conversationUrl || !isConversationUrl(conversationUrl)) {
880
+ throw error;
881
+ }
882
+ logger('Assistant response stalled; reloading conversation and retrying once');
883
+ await Page.navigate({ url: conversationUrl });
884
+ await delay(1000);
885
+ return await waitForAssistantResponse(Runtime, timeoutMs, logger);
886
+ }
887
+ }
888
+ function shouldReloadAfterAssistantError(error) {
889
+ if (!(error instanceof Error))
890
+ return false;
891
+ const message = error.message.toLowerCase();
892
+ return message.includes('assistant-response') || message.includes('watchdog') || message.includes('timeout');
893
+ }
894
+ async function readConversationUrl(Runtime) {
895
+ try {
896
+ const currentUrl = await Runtime.evaluate({ expression: 'location.href', returnByValue: true });
897
+ return typeof currentUrl.result?.value === 'string' ? currentUrl.result.value : null;
898
+ }
899
+ catch {
900
+ return null;
901
+ }
902
+ }
903
+ function isConversationUrl(url) {
904
+ return /\/c\/[a-z0-9-]+/i.test(url);
905
+ }
862
906
  function startThinkingStatusMonitor(Runtime, logger, includeDiagnostics = false) {
863
907
  let stopped = false;
864
908
  let pending = false;
@@ -947,6 +991,10 @@ function isWsl() {
947
991
  return true;
948
992
  return os.release().toLowerCase().includes('microsoft');
949
993
  }
994
+ function extractConversationIdFromUrl(url) {
995
+ const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
996
+ return match?.[1];
997
+ }
950
998
  async function resolveUserDataBaseDir() {
951
999
  // On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
952
1000
  if (isWsl()) {