@steipete/oracle 0.12.0 → 0.13.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.
@@ -11,9 +11,11 @@ import { buildClickDispatcher } from "./domEvents.js";
11
11
  *
12
12
  * @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
13
13
  */
14
- export async function ensureThinkingTime(Runtime, level, logger) {
15
- const result = await evaluateThinkingTimeSelection(Runtime, level);
14
+ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
15
+ const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
16
16
  const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
17
+ const targetModelKind = inferThinkingTargetModelKind(desiredModel);
18
+ const strictProEffort = targetModelKind === "pro" && level === "extended";
17
19
  switch (result?.status) {
18
20
  case "already-selected":
19
21
  logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
@@ -23,13 +25,26 @@ export async function ensureThinkingTime(Runtime, level, logger) {
23
25
  return;
24
26
  case "chip-not-found":
25
27
  case "menu-not-found":
26
- case "option-not-found": {
28
+ case "option-not-found":
29
+ case "model-kind-not-found": {
27
30
  await logDomFailure(Runtime, logger, `thinking-${result.status}`);
28
- logger(`Thinking time: ${result.status.replaceAll("-", " ")} (requested ${capitalizedLevel}); continuing with ChatGPT default.`);
31
+ const kindHint = result.status === "model-kind-not-found" && result.modelKind
32
+ ? ` for ${result.modelKind}`
33
+ : targetModelKind
34
+ ? ` for ${targetModelKind}`
35
+ : "";
36
+ const message = `Thinking time: ${result.status.replaceAll("-", " ")}${kindHint} (requested ${capitalizedLevel})`;
37
+ if (strictProEffort) {
38
+ throw new Error(`${message}; refusing to submit without confirmed Pro Extended.`);
39
+ }
40
+ logger(`${message}; continuing with ChatGPT default.`);
29
41
  return;
30
42
  }
31
43
  default: {
32
44
  await logDomFailure(Runtime, logger, "thinking-time-unknown");
45
+ if (strictProEffort) {
46
+ throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed Pro Extended.`);
47
+ }
33
48
  logger(`Thinking time: unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`);
34
49
  return;
35
50
  }
@@ -40,9 +55,9 @@ export async function ensureThinkingTime(Runtime, level, logger) {
40
55
  * Safe by default: if the pill/menu/option isn't present, we continue without throwing.
41
56
  * @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
42
57
  */
43
- export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
58
+ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desiredModel) {
44
59
  try {
45
- const result = await evaluateThinkingTimeSelection(Runtime, level);
60
+ const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
46
61
  const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
47
62
  switch (result?.status) {
48
63
  case "already-selected":
@@ -54,6 +69,7 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
54
69
  case "chip-not-found":
55
70
  case "menu-not-found":
56
71
  case "option-not-found":
72
+ case "model-kind-not-found":
57
73
  if (logger.verbose) {
58
74
  logger(`Thinking time: ${result.status.replaceAll("-", " ")}; continuing with default.`);
59
75
  }
@@ -74,19 +90,20 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
74
90
  return false;
75
91
  }
76
92
  }
77
- async function evaluateThinkingTimeSelection(Runtime, level) {
93
+ async function evaluateThinkingTimeSelection(Runtime, level, desiredModel) {
78
94
  const outcome = await Runtime.evaluate({
79
- expression: buildThinkingTimeExpression(level),
95
+ expression: buildThinkingTimeExpression(level, desiredModel),
80
96
  awaitPromise: true,
81
97
  returnByValue: true,
82
98
  });
83
99
  return outcome.result?.value;
84
100
  }
85
- function buildThinkingTimeExpression(level) {
101
+ function buildThinkingTimeExpression(level, desiredModel) {
86
102
  const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
87
103
  const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
88
104
  const modelButtonLiteral = JSON.stringify(MODEL_BUTTON_SELECTOR);
89
105
  const targetLevelLiteral = JSON.stringify(level.toLowerCase());
106
+ const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
90
107
  return `(async () => {
91
108
  ${buildClickDispatcher()}
92
109
 
@@ -94,6 +111,7 @@ function buildThinkingTimeExpression(level) {
94
111
  const MENU_ITEM_SELECTOR = ${menuItemLiteral};
95
112
  const MODEL_BUTTON_SELECTOR = ${modelButtonLiteral};
96
113
  const TARGET_LEVEL = ${targetLevelLiteral};
114
+ const TARGET_MODEL_KIND = ${targetModelKindLiteral};
97
115
 
98
116
  // Bilingual matchers: English level token + observed Chinese variants.
99
117
  const LEVEL_TOKENS = {
@@ -119,6 +137,7 @@ function buildThinkingTimeExpression(level) {
119
137
  const t = normalize(text);
120
138
  return targetTokens.some((tok) => t.includes(String(tok).toLowerCase()));
121
139
  };
140
+ const hasToken = (text, token) => normalize(text).split(' ').includes(token);
122
141
  const optionIsSelected = (node) => {
123
142
  if (!(node instanceof HTMLElement)) return false;
124
143
  const ariaChecked = node.getAttribute('aria-checked');
@@ -207,6 +226,7 @@ function buildThinkingTimeExpression(level) {
207
226
 
208
227
  const findModelButton = () => document.querySelector(MODEL_BUTTON_SELECTOR);
209
228
  const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
229
+ const KIND_NOT_FOUND = { kindNotFound: true };
210
230
  const findEffortRow = (node) => {
211
231
  let current = node instanceof HTMLElement ? node.parentElement : null;
212
232
  while (current && current !== document.body) {
@@ -227,6 +247,58 @@ function buildThinkingTimeExpression(level) {
227
247
  ),
228
248
  );
229
249
  };
250
+ const rowForTrailing = (trailing) =>
251
+ trailing.closest('[role="menuitem"], [role="menuitemradio"], [data-radix-collection-item]');
252
+ const rowTextForTrailing = (trailing) => {
253
+ const row = rowForTrailing(trailing) || findEffortRow(trailing);
254
+ return normalize(
255
+ (row?.getAttribute?.('aria-label') ?? '') + ' ' +
256
+ (row?.getAttribute?.('data-testid') ?? '') + ' ' +
257
+ (row?.textContent ?? '') + ' ' +
258
+ (trailing.getAttribute?.('aria-label') ?? '') + ' ' +
259
+ (trailing.getAttribute?.('data-testid') ?? '')
260
+ );
261
+ };
262
+ const testIdTextForTrailing = (trailing) => {
263
+ const row = rowForTrailing(trailing) || findEffortRow(trailing);
264
+ return normalize(
265
+ (row?.getAttribute?.('data-testid') ?? '') + ' ' +
266
+ (trailing.getAttribute?.('data-testid') ?? '')
267
+ );
268
+ };
269
+ const modelKindFromTrailing = (trailing) => {
270
+ const idText = testIdTextForTrailing(trailing);
271
+ if (!idText.includes('model switcher')) return null;
272
+ const modelPart = normalize(idText.replace(/\\bthinking effort\\b.*$/, ''));
273
+ if (hasToken(modelPart, 'pro')) return 'pro';
274
+ if (hasToken(modelPart, 'thinking')) return 'thinking';
275
+ if (hasToken(modelPart, 'instant')) return 'instant';
276
+ return null;
277
+ };
278
+ const trailingMatchesTargetModelKind = (trailing) => {
279
+ if (!TARGET_MODEL_KIND) return false;
280
+ const idKind = modelKindFromTrailing(trailing);
281
+ if (idKind) return idKind === TARGET_MODEL_KIND;
282
+ const text = rowTextForTrailing(trailing);
283
+ if (TARGET_MODEL_KIND === 'pro') {
284
+ return hasToken(text, 'pro') && !hasToken(text, 'thinking');
285
+ }
286
+ if (TARGET_MODEL_KIND === 'thinking') {
287
+ return hasToken(text, 'thinking') && !hasToken(text, 'pro');
288
+ }
289
+ if (TARGET_MODEL_KIND === 'instant') {
290
+ return hasToken(text, 'instant') && !hasToken(text, 'thinking') && !hasToken(text, 'pro');
291
+ }
292
+ return false;
293
+ };
294
+ const hasStableBox = (node) => {
295
+ const r = node.getBoundingClientRect?.();
296
+ return Boolean(r && r.width > 0 && r.height > 0 && node.getAttribute?.('aria-hidden') !== 'true');
297
+ };
298
+ const pickSingleStableTrailing = (trailings) => {
299
+ const visible = trailings.filter((t) => hasStableBox(t));
300
+ return visible.length === 1 ? visible[0] : null;
301
+ };
230
302
  const pickTrailingForCurrentModel = () => {
231
303
  const trailings = findTrailingButtons();
232
304
  if (trailings.length === 0) return null;
@@ -236,6 +308,10 @@ function buildThinkingTimeExpression(level) {
236
308
  const row = findEffortRow(t);
237
309
  if (rowIsSelected(row)) return t;
238
310
  }
311
+ if (TARGET_MODEL_KIND) {
312
+ const targetTrailings = trailings.filter((t) => trailingMatchesTargetModelKind(t));
313
+ return pickSingleStableTrailing(targetTrailings) || KIND_NOT_FOUND;
314
+ }
239
315
  return null;
240
316
  };
241
317
 
@@ -243,7 +319,6 @@ function buildThinkingTimeExpression(level) {
243
319
  if (!modelBtn) {
244
320
  return { status: 'chip-not-found' };
245
321
  }
246
-
247
322
  // Open model menu (idempotent — leaves it open if already open).
248
323
  if (modelBtn.getAttribute('aria-expanded') !== 'true') {
249
324
  dispatchClickSequence(modelBtn);
@@ -261,6 +336,10 @@ function buildThinkingTimeExpression(level) {
261
336
  closeOpenMenus();
262
337
  return { status: 'chip-not-found' };
263
338
  }
339
+ if (trailing.kindNotFound) {
340
+ closeOpenMenus();
341
+ return { status: 'model-kind-not-found', modelKind: TARGET_MODEL_KIND };
342
+ }
264
343
 
265
344
  dispatchClickSequence(trailing);
266
345
  await sleep(STEP_WAIT_MS);
@@ -313,6 +392,26 @@ function buildThinkingTimeExpression(level) {
313
392
  return { status: already ? 'already-selected' : 'switched', label };
314
393
  })()`;
315
394
  }
316
- export function buildThinkingTimeExpressionForTest(level = "extended") {
317
- return buildThinkingTimeExpression(level);
395
+ export function buildThinkingTimeExpressionForTest(level = "extended", desiredModel) {
396
+ return buildThinkingTimeExpression(level, desiredModel);
397
+ }
398
+ function inferThinkingTargetModelKind(desiredModel) {
399
+ const normalized = (desiredModel ?? "")
400
+ .toLowerCase()
401
+ .replace(/[^a-z0-9]+/g, " ")
402
+ .replace(/\s+/g, " ")
403
+ .trim();
404
+ if (!normalized)
405
+ return null;
406
+ const tokens = normalized.split(" ");
407
+ if (tokens.includes("pro"))
408
+ return "pro";
409
+ if (tokens.includes("thinking"))
410
+ return "thinking";
411
+ if (tokens.includes("instant"))
412
+ return "instant";
413
+ return null;
414
+ }
415
+ export function inferThinkingTargetModelKindForTest(desiredModel) {
416
+ return inferThinkingTargetModelKind(desiredModel);
318
417
  }
@@ -26,6 +26,7 @@ export const DEFAULT_BROWSER_CONFIG = {
26
26
  timeoutMs: 1_200_000,
27
27
  debugPort: null,
28
28
  inputTimeoutMs: 60_000,
29
+ attachmentTimeoutMs: 45_000,
29
30
  assistantRecheckDelayMs: 0,
30
31
  assistantRecheckTimeoutMs: 120_000,
31
32
  reuseChromeWaitMs: 10_000,
@@ -80,6 +81,7 @@ export function resolveBrowserConfig(config) {
80
81
  timeoutMs: config?.timeoutMs ?? defaultTimeoutMs,
81
82
  debugPort: config?.debugPort ?? debugPortEnv ?? DEFAULT_BROWSER_CONFIG.debugPort,
82
83
  inputTimeoutMs: config?.inputTimeoutMs ?? DEFAULT_BROWSER_CONFIG.inputTimeoutMs,
84
+ attachmentTimeoutMs: config?.attachmentTimeoutMs ?? DEFAULT_BROWSER_CONFIG.attachmentTimeoutMs,
83
85
  assistantRecheckDelayMs: config?.assistantRecheckDelayMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckDelayMs,
84
86
  assistantRecheckTimeoutMs: config?.assistantRecheckTimeoutMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckTimeoutMs,
85
87
  reuseChromeWaitMs: config?.reuseChromeWaitMs ?? DEFAULT_BROWSER_CONFIG.reuseChromeWaitMs,
@@ -327,6 +327,7 @@ export async function runBrowserMode(options) {
327
327
  const runtimeHintCb = options.runtimeHintCb;
328
328
  let lastTargetId;
329
329
  let lastUrl;
330
+ let promptSubmitted = false;
330
331
  let tabLease = null;
331
332
  const emitRuntimeHint = async () => {
332
333
  if (!chrome?.port) {
@@ -340,6 +341,7 @@ export async function runBrowserMode(options) {
340
341
  chromeTargetId: lastTargetId,
341
342
  tabUrl: lastUrl,
342
343
  conversationId,
344
+ promptSubmitted,
343
345
  userDataDir,
344
346
  controllerPid: process.pid,
345
347
  };
@@ -357,6 +359,13 @@ export async function runBrowserMode(options) {
357
359
  logger(`Failed to persist runtime hint: ${message}`);
358
360
  }
359
361
  };
362
+ const markPromptSubmitted = async () => {
363
+ if (promptSubmitted) {
364
+ return;
365
+ }
366
+ promptSubmitted = true;
367
+ await emitRuntimeHint();
368
+ };
360
369
  if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
361
370
  logger(`[browser-mode] config: ${JSON.stringify({
362
371
  ...redactBrowserConfigForDebugLog(config),
@@ -725,7 +734,8 @@ export async function runBrowserMode(options) {
725
734
  // Handle thinking time selection if specified. Deep Research owns its own effort flow.
726
735
  const thinkingTime = config.thinkingTime;
727
736
  if (thinkingTime && !deepResearch) {
728
- await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
737
+ const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
738
+ await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
729
739
  retries: 2,
730
740
  delayMs: 300,
731
741
  onRetry: (attempt, error) => {
@@ -790,7 +800,8 @@ export async function runBrowserMode(options) {
790
800
  const baseTimeout = config.inputTimeoutMs ?? 30_000;
791
801
  const perFileTimeout = 20_000;
792
802
  const waitBudget = Math.max(baseTimeout, 45_000) + (submissionAttachments.length - 1) * perFileTimeout;
793
- await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
803
+ const attachmentWaitBudget = Math.max(config.attachmentTimeoutMs ?? 0, waitBudget);
804
+ await waitForAttachmentCompletion(Runtime, attachmentWaitBudget, attachmentNames, logger);
794
805
  logger("All attachments uploaded");
795
806
  }
796
807
  let baselineTurns = await readConversationTurnCount(Runtime, logger);
@@ -801,8 +812,10 @@ export async function runBrowserMode(options) {
801
812
  logger,
802
813
  timeoutMs: config.timeoutMs,
803
814
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
815
+ attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
804
816
  baselineTurns: baselineTurns ?? undefined,
805
817
  attachmentNames,
818
+ onPromptSubmitted: markPromptSubmitted,
806
819
  };
807
820
  await runProviderSubmissionFlow(chatgptDomProvider, {
808
821
  prompt,
@@ -811,6 +824,7 @@ export async function runBrowserMode(options) {
811
824
  log: logger,
812
825
  state: providerState,
813
826
  });
827
+ await markPromptSubmitted();
814
828
  const providerBaselineTurns = providerState.baselineTurns;
815
829
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
816
830
  baselineTurns = providerBaselineTurns;
@@ -912,6 +926,7 @@ export async function runBrowserMode(options) {
912
926
  chromeTargetId: lastTargetId,
913
927
  tabUrl: lastUrl,
914
928
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
929
+ promptSubmitted,
915
930
  controllerPid: process.pid,
916
931
  };
917
932
  }
@@ -996,6 +1011,7 @@ export async function runBrowserMode(options) {
996
1011
  chromeTargetId: lastTargetId,
997
1012
  tabUrl: lastUrl,
998
1013
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1014
+ promptSubmitted,
999
1015
  controllerPid: process.pid,
1000
1016
  },
1001
1017
  });
@@ -1051,6 +1067,7 @@ export async function runBrowserMode(options) {
1051
1067
  chromeTargetId: lastTargetId,
1052
1068
  tabUrl: lastUrl,
1053
1069
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1070
+ promptSubmitted,
1054
1071
  controllerPid: process.pid,
1055
1072
  };
1056
1073
  throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
@@ -1298,6 +1315,7 @@ export async function runBrowserMode(options) {
1298
1315
  chromeTargetId: lastTargetId,
1299
1316
  tabUrl: lastUrl,
1300
1317
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1318
+ promptSubmitted,
1301
1319
  controllerPid: process.pid,
1302
1320
  };
1303
1321
  }
@@ -1315,6 +1333,7 @@ export async function runBrowserMode(options) {
1315
1333
  userDataDir,
1316
1334
  chromeTargetId: lastTargetId,
1317
1335
  tabUrl: lastUrl,
1336
+ promptSubmitted,
1318
1337
  controllerPid: process.pid,
1319
1338
  };
1320
1339
  const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
@@ -1355,6 +1374,7 @@ export async function runBrowserMode(options) {
1355
1374
  userDataDir,
1356
1375
  chromeTargetId: lastTargetId,
1357
1376
  tabUrl: lastUrl,
1377
+ promptSubmitted,
1358
1378
  controllerPid: process.pid,
1359
1379
  },
1360
1380
  }, normalizedError);
@@ -1703,6 +1723,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1703
1723
  let remoteTargetId = null;
1704
1724
  let tabLease = null;
1705
1725
  let lastUrl;
1726
+ let promptSubmitted = false;
1706
1727
  let attachedExistingTab = false;
1707
1728
  let ownsTarget = true;
1708
1729
  const runtimeHintCb = options.runtimeHintCb;
@@ -1718,6 +1739,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1718
1739
  chromeTargetId: remoteTargetId ?? undefined,
1719
1740
  tabUrl: lastUrl,
1720
1741
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
1742
+ promptSubmitted,
1721
1743
  controllerPid: process.pid,
1722
1744
  });
1723
1745
  await tabLease?.update({
@@ -1732,6 +1754,13 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1732
1754
  logger(`Failed to persist runtime hint: ${message}`);
1733
1755
  }
1734
1756
  };
1757
+ const markPromptSubmitted = async () => {
1758
+ if (promptSubmitted) {
1759
+ return;
1760
+ }
1761
+ promptSubmitted = true;
1762
+ await emitRuntimeHint();
1763
+ };
1735
1764
  const startedAt = Date.now();
1736
1765
  let answerText = "";
1737
1766
  let answerMarkdown = "";
@@ -1848,7 +1877,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1848
1877
  // Handle thinking time selection if specified. Deep Research owns its own effort flow.
1849
1878
  const thinkingTime = config.thinkingTime;
1850
1879
  if (thinkingTime && !deepResearch) {
1851
- await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
1880
+ const thinkingTargetModel = modelStrategy === "select" ? config.desiredModel : null;
1881
+ await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger, thinkingTargetModel), {
1852
1882
  retries: 2,
1853
1883
  delayMs: 300,
1854
1884
  onRetry: (attempt, error) => {
@@ -1892,7 +1922,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1892
1922
  const baseTimeout = config.inputTimeoutMs ?? 30_000;
1893
1923
  const perFileTimeout = 15_000;
1894
1924
  const waitBudget = Math.max(baseTimeout, 30_000) + (submissionAttachments.length - 1) * perFileTimeout;
1895
- await waitForAttachmentCompletion(Runtime, waitBudget, attachmentNames, logger);
1925
+ const attachmentWaitBudget = Math.max(config.attachmentTimeoutMs ?? 0, waitBudget);
1926
+ await waitForAttachmentCompletion(Runtime, attachmentWaitBudget, attachmentNames, logger);
1896
1927
  logger("All attachments uploaded");
1897
1928
  }
1898
1929
  let baselineTurns = await readConversationTurnCount(Runtime, logger);
@@ -1902,8 +1933,10 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1902
1933
  logger,
1903
1934
  timeoutMs: config.timeoutMs,
1904
1935
  inputTimeoutMs: config.inputTimeoutMs ?? undefined,
1936
+ attachmentTimeoutMs: config.attachmentTimeoutMs ?? undefined,
1905
1937
  baselineTurns: baselineTurns ?? undefined,
1906
1938
  attachmentNames,
1939
+ onPromptSubmitted: markPromptSubmitted,
1907
1940
  };
1908
1941
  await runProviderSubmissionFlow(chatgptDomProvider, {
1909
1942
  prompt,
@@ -1912,6 +1945,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1912
1945
  log: logger,
1913
1946
  state: providerState,
1914
1947
  });
1948
+ await markPromptSubmitted();
1915
1949
  const providerBaselineTurns = providerState.baselineTurns;
1916
1950
  if (typeof providerBaselineTurns === "number" && Number.isFinite(providerBaselineTurns)) {
1917
1951
  baselineTurns = providerBaselineTurns;
@@ -1985,6 +2019,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1985
2019
  chromeTargetId: remoteTargetId ?? undefined,
1986
2020
  tabUrl: lastUrl,
1987
2021
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2022
+ promptSubmitted,
1988
2023
  controllerPid: process.pid,
1989
2024
  };
1990
2025
  }
@@ -2068,6 +2103,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2068
2103
  chromeTargetId: remoteTargetId ?? undefined,
2069
2104
  tabUrl: lastUrl,
2070
2105
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2106
+ promptSubmitted,
2071
2107
  controllerPid: process.pid,
2072
2108
  },
2073
2109
  });
@@ -2136,6 +2172,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2136
2172
  chromeTargetId: remoteTargetId ?? undefined,
2137
2173
  tabUrl: lastUrl,
2138
2174
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2175
+ promptSubmitted,
2139
2176
  controllerPid: process.pid,
2140
2177
  };
2141
2178
  throw new BrowserAutomationError("Assistant response timed out before completion; reattach later to capture the answer.", { stage: "assistant-timeout", runtime, diagnostics }, error);
@@ -2338,6 +2375,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2338
2375
  chromeTargetId: remoteTargetId ?? undefined,
2339
2376
  tabUrl: lastUrl,
2340
2377
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2378
+ promptSubmitted,
2341
2379
  artifacts: savedArtifacts,
2342
2380
  archive,
2343
2381
  modelSelection: modelSelectionEvidence,
@@ -2364,6 +2402,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2364
2402
  chromeProfileRoot,
2365
2403
  chromeTargetId: remoteTargetId ?? undefined,
2366
2404
  tabUrl: lastUrl,
2405
+ promptSubmitted,
2367
2406
  controllerPid: process.pid,
2368
2407
  },
2369
2408
  });
@@ -23,6 +23,8 @@ async function submitPromptViaAdapter(ctx) {
23
23
  attachmentNames: state.attachmentNames ?? [],
24
24
  baselineTurns: state.baselineTurns ?? undefined,
25
25
  inputTimeoutMs: state.inputTimeoutMs ?? undefined,
26
+ attachmentTimeoutMs: state.attachmentTimeoutMs ?? undefined,
27
+ onPromptSubmitted: state.onPromptSubmitted,
26
28
  }, ctx.prompt, state.logger);
27
29
  state.committedTurns =
28
30
  typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;
@@ -0,0 +1,22 @@
1
+ export function hasRecoverableChatGptConversation(runtime) {
2
+ if (!runtime) {
3
+ return false;
4
+ }
5
+ if (runtime.conversationId?.trim()) {
6
+ return true;
7
+ }
8
+ const tabUrl = runtime.tabUrl?.trim();
9
+ if (!tabUrl) {
10
+ return false;
11
+ }
12
+ try {
13
+ const url = new URL(tabUrl);
14
+ if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
15
+ return false;
16
+ }
17
+ return /(?:^|\/)c\/[^/]+/.test(url.pathname);
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
@@ -214,6 +214,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
214
214
  chromeTargetId: browserResult.chromeTargetId,
215
215
  tabUrl: browserResult.tabUrl,
216
216
  conversationId: browserResult.conversationId,
217
+ promptSubmitted: browserResult.promptSubmitted,
217
218
  controllerPid: browserResult.controllerPid ?? process.pid,
218
219
  },
219
220
  archive: browserResult.archive,
@@ -7,8 +7,9 @@ import { checkTcpConnection, checkRemoteHealth } from "../../remote/health.js";
7
7
  import { detectChromeBinary, detectChromeCookieDb } from "../../browser/detect.js";
8
8
  import { formatCodexMcpSnippet } from "./codexConfig.js";
9
9
  export async function runBridgeDoctor(_options) {
10
- const { config: userConfig, path: configPath, loaded } = await loadUserConfig();
10
+ const { config: userConfig, path: configPath, paths: configPaths, loaded: userConfigLoaded, } = await loadUserConfig();
11
11
  const version = getCliVersion();
12
+ const projectConfigPaths = configPaths.filter((entry) => entry !== configPath);
12
13
  const resolvedRemote = resolveRemoteServiceConfig({
13
14
  cliHost: undefined,
14
15
  cliToken: undefined,
@@ -22,7 +23,11 @@ export async function runBridgeDoctor(_options) {
22
23
  lines.push(chalk.dim(`OS: ${process.platform} ${os.release()} (${process.arch})`));
23
24
  lines.push(chalk.dim(`Node: ${process.version}`));
24
25
  lines.push(chalk.dim(`Oracle: ${version}`));
25
- lines.push(chalk.dim(`Config: ${loaded ? configPath : "(missing)"}`));
26
+ lines.push(chalk.dim(`Config: ${userConfigLoaded ? configPath : `${configPath} (missing)`}`));
27
+ if (projectConfigPaths.length > 0) {
28
+ const label = projectConfigPaths.length === 1 ? "Project config" : "Project configs";
29
+ lines.push(chalk.dim(`${label}: ${projectConfigPaths.join(", ")}`));
30
+ }
26
31
  if (userConfig.engine) {
27
32
  lines.push(chalk.dim(`Default engine: ${userConfig.engine}`));
28
33
  }
@@ -7,6 +7,7 @@ import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
7
7
  import { getOracleHomeDir } from "../oracleHome.js";
8
8
  const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
9
9
  const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 60_000;
10
+ const DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS = 45_000;
10
11
  const DEFAULT_BROWSER_RECHECK_TIMEOUT_MS = 120_000;
11
12
  const DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS = 120_000;
12
13
  const DEFAULT_CHROME_PROFILE = "Default";
@@ -15,6 +16,7 @@ const DEFAULT_CHROME_PROFILE = "Default";
15
16
  const BROWSER_MODEL_LABELS = [
16
17
  // Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
17
18
  ["gpt-5.5-pro", "Pro"],
19
+ ["gpt-5.5-instant", "GPT-5.5 Instant"],
18
20
  ["gpt-5.5", "Thinking 5.5"],
19
21
  ["gpt-5.4-pro", "Pro"],
20
22
  ["gpt-5.2-thinking", "GPT-5.2 Thinking"],
@@ -34,7 +36,10 @@ export function normalizeChatGptModelForBrowser(model) {
34
36
  if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
35
37
  return model;
36
38
  }
37
- if (normalized === "gpt-5.5-pro" || normalized === "gpt-5.5" || normalized === "gpt-5.4") {
39
+ if (normalized === "gpt-5.5-pro" ||
40
+ normalized === "gpt-5.5-instant" ||
41
+ normalized === "gpt-5.5" ||
42
+ normalized === "gpt-5.4") {
38
43
  return normalized;
39
44
  }
40
45
  // Pro variants: resolve to the latest Pro model in ChatGPT.
@@ -101,6 +106,9 @@ export async function buildBrowserConfig(options) {
101
106
  inputTimeoutMs: options.browserInputTimeout
102
107
  ? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
103
108
  : undefined,
109
+ attachmentTimeoutMs: options.browserAttachmentTimeout
110
+ ? parseDuration(options.browserAttachmentTimeout, DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
111
+ : undefined,
104
112
  assistantRecheckDelayMs: options.browserRecheckDelay
105
113
  ? parseDuration(options.browserRecheckDelay, 0)
106
114
  : undefined,
@@ -43,6 +43,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
43
43
  if (isUnset("browserInputTimeout") && typeof browser.inputTimeoutMs === "number") {
44
44
  options.browserInputTimeout = String(browser.inputTimeoutMs);
45
45
  }
46
+ if (isUnset("browserAttachmentTimeout") && typeof browser.attachmentTimeoutMs === "number") {
47
+ options.browserAttachmentTimeout = String(browser.attachmentTimeoutMs);
48
+ }
46
49
  if (isUnset("browserRecheckDelay") && typeof browser.assistantRecheckDelayMs === "number") {
47
50
  options.browserRecheckDelay = String(browser.assistantRecheckDelayMs);
48
51
  }
@@ -14,9 +14,10 @@ export function defaultWaitPreference(model, engine) {
14
14
  * 2) Explicit --engine value.
15
15
  * 3) Explicit API provider routing flags force API.
16
16
  * 4) ORACLE_ENGINE environment override (api|browser).
17
- * 5) API environment decides: api when set, otherwise browser.
17
+ * 5) Config engine value.
18
+ * 6) API environment decides: api when set, otherwise browser.
18
19
  */
19
- export function resolveEngine({ engine, browserFlag, apiProviderRequested, env, }) {
20
+ export function resolveEngine({ engine, configEngine, browserFlag, apiProviderRequested, env, }) {
20
21
  if (browserFlag) {
21
22
  return "browser";
22
23
  }
@@ -30,6 +31,9 @@ export function resolveEngine({ engine, browserFlag, apiProviderRequested, env,
30
31
  if (envEngine) {
31
32
  return envEngine;
32
33
  }
34
+ if (configEngine) {
35
+ return configEngine;
36
+ }
33
37
  return hasApiEnvironment(env) ? "api" : "browser";
34
38
  }
35
39
  function hasApiEnvironment(env) {
@@ -280,6 +280,10 @@ export function inferModelFromLabel(modelValue) {
280
280
  if ((normalized.includes("5.5") || normalized.includes("5_5")) && normalized.includes("pro")) {
281
281
  return "gpt-5.5-pro";
282
282
  }
283
+ if ((normalized.includes("5.5") || normalized.includes("5_5")) &&
284
+ (normalized.includes("instant") || normalized.includes("fast"))) {
285
+ return "gpt-5.5-instant";
286
+ }
283
287
  if (normalized.includes("5.5") || normalized.includes("5_5")) {
284
288
  return "gpt-5.5";
285
289
  }