@steipete/oracle 0.11.0 → 0.12.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 (49) hide show
  1. package/README.md +56 -11
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/archiveConversation.js +12 -0
  4. package/dist/src/browser/actions/modelSelection.js +61 -18
  5. package/dist/src/browser/actions/navigation.js +5 -3
  6. package/dist/src/browser/actions/promptComposer.js +75 -18
  7. package/dist/src/browser/actions/thinkingTime.js +23 -8
  8. package/dist/src/browser/config.js +1 -7
  9. package/dist/src/browser/constants.js +1 -1
  10. package/dist/src/browser/index.js +65 -48
  11. package/dist/src/browser/manualLoginProfile.js +54 -0
  12. package/dist/src/browser/projectSourcesRunner.js +16 -5
  13. package/dist/src/browser/prompt.js +56 -37
  14. package/dist/src/browser/sessionRunner.js +72 -1
  15. package/dist/src/browser/utils.js +1 -47
  16. package/dist/src/browser/zipBundle.js +152 -0
  17. package/dist/src/cli/browserConfig.js +13 -18
  18. package/dist/src/cli/browserDefaults.js +2 -1
  19. package/dist/src/cli/docsCheck.js +186 -0
  20. package/dist/src/cli/engine.js +11 -4
  21. package/dist/src/cli/options.js +12 -6
  22. package/dist/src/cli/perfTrace.js +242 -0
  23. package/dist/src/cli/promptRequirement.js +2 -0
  24. package/dist/src/cli/providerDoctor.js +85 -0
  25. package/dist/src/cli/runOptions.js +46 -16
  26. package/dist/src/cli/sessionDisplay.js +39 -4
  27. package/dist/src/cli/sessionLifecycle.js +38 -0
  28. package/dist/src/cli/sessionRunner.js +228 -3
  29. package/dist/src/cli/sessionTable.js +2 -1
  30. package/dist/src/duration.js +47 -0
  31. package/dist/src/mcp/tools/consult.js +19 -3
  32. package/dist/src/mcp/types.js +5 -2
  33. package/dist/src/mcp/utils.js +4 -1
  34. package/dist/src/oracle/baseUrl.js +17 -0
  35. package/dist/src/oracle/client.js +1 -22
  36. package/dist/src/oracle/config.js +17 -4
  37. package/dist/src/oracle/gemini.js +2 -22
  38. package/dist/src/oracle/geminiModels.js +21 -0
  39. package/dist/src/oracle/modelResolver.js +7 -1
  40. package/dist/src/oracle/multiModelRunner.js +20 -2
  41. package/dist/src/oracle/providerFailures.js +204 -0
  42. package/dist/src/oracle/providerRoutePlan.js +281 -0
  43. package/dist/src/oracle/providerRouting.js +92 -0
  44. package/dist/src/oracle/run.js +157 -54
  45. package/dist/src/oracle.js +1 -0
  46. package/dist/src/remote/client.js +8 -0
  47. package/dist/src/remote/server.js +26 -0
  48. package/dist/src/sessionManager.js +5 -1
  49. package/package.json +8 -6
@@ -26,6 +26,7 @@ import { resolveAttachRunningConnection } from "./attachRunning.js";
26
26
  import { connectToExistingChatGptTab } from "./liveTabs.js";
27
27
  import { captureBrowserDiagnostics } from "./domDebug.js";
28
28
  import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./actions/archiveConversation.js";
29
+ import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
29
30
  import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
30
31
  export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
31
32
  export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
@@ -70,19 +71,12 @@ export function shouldPreserveBrowserOnErrorForTest(error, headless) {
70
71
  export function classifyPreservedBrowserErrorForTest(error, headless) {
71
72
  return classifyPreservedBrowserError(error, headless);
72
73
  }
73
- function shouldSkipThinkingTimeSelection(desiredModel, thinkingTime) {
74
- if (thinkingTime !== "extended" || !desiredModel) {
75
- return false;
76
- }
77
- const normalized = desiredModel.toLowerCase();
78
- return (normalized === "gpt-5.5-pro" ||
79
- normalized.includes("gpt-5.5 pro") ||
80
- normalized.includes("gpt 5.5 pro") ||
81
- normalized.includes("gpt 5 5 pro"));
82
- }
83
- export function shouldSkipThinkingTimeSelectionForTest(desiredModel, thinkingTime) {
84
- return shouldSkipThinkingTimeSelection(desiredModel, thinkingTime);
85
- }
74
+ // NOTE: Previously, shouldSkipThinkingTimeSelection() would skip the thinking
75
+ // time UI step when desiredModel was gpt-5.5-pro and thinkingTime was "extended",
76
+ // assuming that selecting "Pro Extended" in the old UI already implied Extended
77
+ // effort. This is wrong for lower-tier plans ($100/mo Pro) where selecting "Pro"
78
+ // defaults to Standard effort. ensureThinkingTime() already handles the
79
+ // "already-selected" case as a no-op, so always attempting it is safe.
86
80
  function listIgnoredRemoteChromeFlags(config) {
87
81
  return [
88
82
  config.headless ? "--browser-headless" : null,
@@ -297,6 +291,17 @@ async function closeRemoteConnectionAfterRun(options) {
297
291
  function shouldCloseOwnedRunTargetAfterRun(options) {
298
292
  return options.runStatus === "complete" && options.ownsTarget && !options.keepBrowser;
299
293
  }
294
+ function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
295
+ return {
296
+ requestedModel: desiredModel ?? null,
297
+ resolvedLabel: null,
298
+ strategy,
299
+ status: "skipped",
300
+ verified: false,
301
+ source: "config",
302
+ capturedAt: new Date().toISOString(),
303
+ };
304
+ }
300
305
  export async function runBrowserMode(options) {
301
306
  const promptText = options.prompt?.trim();
302
307
  if (!promptText) {
@@ -390,14 +395,19 @@ export async function runBrowserMode(options) {
390
395
  const manualLogin = Boolean(config.manualLogin);
391
396
  const manualProfileDir = config.manualLoginProfileDir
392
397
  ? path.resolve(config.manualLoginProfileDir)
393
- : path.join(os.homedir(), ".oracle", "browser-profile");
398
+ : defaultManualLoginProfileDir();
394
399
  const userDataDir = manualLogin
395
400
  ? manualProfileDir
396
401
  : await mkdtemp(path.join(await resolveUserDataBaseDir(), "oracle-browser-"));
402
+ const effectiveKeepBrowser = Boolean(config.keepBrowser);
397
403
  if (manualLogin) {
398
404
  // Learned: manual login reuses a persistent profile so cookies/SSO survive.
399
405
  await mkdir(userDataDir, { recursive: true });
400
406
  logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
407
+ await assertManualLoginProfileReadyForRun({
408
+ userDataDir,
409
+ keepBrowser: effectiveKeepBrowser,
410
+ });
401
411
  }
402
412
  else {
403
413
  logger(`Created temporary Chrome profile at ${userDataDir}`);
@@ -410,7 +420,6 @@ export async function runBrowserMode(options) {
410
420
  sessionId: options.sessionId,
411
421
  });
412
422
  }
413
- const effectiveKeepBrowser = Boolean(config.keepBrowser);
414
423
  let acquiredChrome;
415
424
  try {
416
425
  acquiredChrome = manualLogin
@@ -458,6 +467,7 @@ export async function runBrowserMode(options) {
458
467
  let answerMarkdown = "";
459
468
  let answerHtml = "";
460
469
  let runStatus = "attempted";
470
+ let modelSelectionEvidence;
461
471
  let connectionClosedUnexpectedly = false;
462
472
  let stopThinkingMonitor = null;
463
473
  let removeDialogHandler = null;
@@ -594,6 +604,8 @@ export async function runBrowserMode(options) {
594
604
  appliedCookies,
595
605
  manualLogin,
596
606
  timeoutMs: config.timeoutMs,
607
+ profileDir: userDataDir,
608
+ keepBrowser: effectiveKeepBrowser,
597
609
  }));
598
610
  if (config.url !== baseUrl) {
599
611
  await raceWithDisconnect(navigateToPromptReadyWithFallback(Page, Runtime, {
@@ -687,7 +699,7 @@ export async function runBrowserMode(options) {
687
699
  await captureRuntimeSnapshot();
688
700
  const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
689
701
  if (config.desiredModel && modelStrategy !== "ignore") {
690
- await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
702
+ modelSelectionEvidence = await raceWithDisconnect(withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
691
703
  retries: 2,
692
704
  delayMs: 300,
693
705
  onRetry: (attempt, error) => {
@@ -706,26 +718,22 @@ export async function runBrowserMode(options) {
706
718
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
707
719
  }
708
720
  else if (modelStrategy === "ignore") {
721
+ modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
709
722
  logger("Model picker: skipped (strategy=ignore)");
710
723
  }
711
724
  const deepResearch = config.researchMode === "deep";
712
725
  // Handle thinking time selection if specified. Deep Research owns its own effort flow.
713
726
  const thinkingTime = config.thinkingTime;
714
727
  if (thinkingTime && !deepResearch) {
715
- if (shouldSkipThinkingTimeSelection(config.desiredModel, thinkingTime)) {
716
- logger("Thinking time: Pro Extended (via model selection)");
717
- }
718
- else {
719
- await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
720
- retries: 2,
721
- delayMs: 300,
722
- onRetry: (attempt, error) => {
723
- if (options.verbose) {
724
- logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
725
- }
726
- },
727
- }));
728
- }
728
+ await raceWithDisconnect(withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
729
+ retries: 2,
730
+ delayMs: 300,
731
+ onRetry: (attempt, error) => {
732
+ if (options.verbose) {
733
+ logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
734
+ }
735
+ },
736
+ }));
729
737
  }
730
738
  if (deepResearch) {
731
739
  await raceWithDisconnect(withRetries(() => activateDeepResearch(Runtime, Input, logger), {
@@ -893,6 +901,7 @@ export async function runBrowserMode(options) {
893
901
  answerHtml: researchResult.html,
894
902
  artifacts: savedArtifacts,
895
903
  archive,
904
+ modelSelection: modelSelectionEvidence,
896
905
  tookMs: durationMs,
897
906
  answerTokens: tokens,
898
907
  answerChars: researchResult.text.length,
@@ -1278,6 +1287,7 @@ export async function runBrowserMode(options) {
1278
1287
  generatedImages: imageArtifacts.generatedImages,
1279
1288
  savedImages: imageArtifacts.savedImages,
1280
1289
  archive,
1290
+ modelSelection: modelSelectionEvidence,
1281
1291
  tookMs: durationMs,
1282
1292
  answerTokens,
1283
1293
  answerChars,
@@ -1503,12 +1513,13 @@ async function findEphemeralPort() {
1503
1513
  });
1504
1514
  });
1505
1515
  }
1506
- async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
1516
+ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, profileDir, keepBrowser, }) {
1507
1517
  if (!manualLogin) {
1508
1518
  await ensureLoggedIn(runtime, logger, { appliedCookies });
1509
1519
  return;
1510
1520
  }
1511
- const deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
1521
+ const waitMs = resolveManualLoginWaitMs(timeoutMs, Boolean(keepBrowser));
1522
+ const deadline = Date.now() + waitMs;
1512
1523
  let lastNotice = 0;
1513
1524
  while (Date.now() < deadline) {
1514
1525
  try {
@@ -1530,7 +1541,10 @@ async function waitForLogin({ runtime, logger, appliedCookies, manualLogin, time
1530
1541
  await delay(1000);
1531
1542
  }
1532
1543
  }
1533
- throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
1544
+ const setupCommand = formatManualLoginSetupCommand(profileDir ?? defaultManualLoginProfileDir());
1545
+ throw new Error("Manual login mode timed out waiting for ChatGPT session. " +
1546
+ `Browser mode is using Oracle's private Chrome profile at ${profileDir ?? "(default profile)"}, not your normal Chrome profile. ` +
1547
+ `Run first-time setup, sign in there, then retry: ${setupCommand}`);
1534
1548
  }
1535
1549
  async function maybeRecoverLongAssistantResponse({ runtime, baselineTurns, answerText, answerMarkdown, logger, allowMarkdownUpdate, }) {
1536
1550
  // Learned: long streaming responses can still be rendering after initial capture.
@@ -1724,6 +1738,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1724
1738
  let answerHtml = "";
1725
1739
  let connectionClosedUnexpectedly = false;
1726
1740
  let runStatus = "attempted";
1741
+ let modelSelectionEvidence;
1727
1742
  let stopThinkingMonitor = null;
1728
1743
  let removeDialogHandler = null;
1729
1744
  let connection = null;
@@ -1813,7 +1828,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1813
1828
  }
1814
1829
  const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
1815
1830
  if (config.desiredModel && modelStrategy !== "ignore") {
1816
- await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
1831
+ modelSelectionEvidence = await withRetries(() => ensureModelSelection(Runtime, config.desiredModel, logger, modelStrategy), {
1817
1832
  retries: 2,
1818
1833
  delayMs: 300,
1819
1834
  onRetry: (attempt, error) => {
@@ -1826,26 +1841,22 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1826
1841
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
1827
1842
  }
1828
1843
  else if (modelStrategy === "ignore") {
1844
+ modelSelectionEvidence = buildSkippedModelSelectionEvidence(config.desiredModel, modelStrategy);
1829
1845
  logger("Model picker: skipped (strategy=ignore)");
1830
1846
  }
1831
1847
  const deepResearch = config.researchMode === "deep";
1832
1848
  // Handle thinking time selection if specified. Deep Research owns its own effort flow.
1833
1849
  const thinkingTime = config.thinkingTime;
1834
1850
  if (thinkingTime && !deepResearch) {
1835
- if (shouldSkipThinkingTimeSelection(config.desiredModel, thinkingTime)) {
1836
- logger("Thinking time: Pro Extended (via model selection)");
1837
- }
1838
- else {
1839
- await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
1840
- retries: 2,
1841
- delayMs: 300,
1842
- onRetry: (attempt, error) => {
1843
- if (options.verbose) {
1844
- logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
1845
- }
1846
- },
1847
- });
1848
- }
1851
+ await withRetries(() => ensureThinkingTime(Runtime, thinkingTime, logger), {
1852
+ retries: 2,
1853
+ delayMs: 300,
1854
+ onRetry: (attempt, error) => {
1855
+ if (options.verbose) {
1856
+ logger(`[retry] Thinking time (${thinkingTime}) attempt ${attempt + 1}: ${error instanceof Error ? error.message : error}`);
1857
+ }
1858
+ },
1859
+ });
1849
1860
  }
1850
1861
  if (deepResearch) {
1851
1862
  await withRetries(() => activateDeepResearch(Runtime, Input, logger), {
@@ -1965,6 +1976,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
1965
1976
  answerHtml: researchResult.html,
1966
1977
  artifacts: savedArtifacts,
1967
1978
  archive,
1979
+ modelSelection: modelSelectionEvidence,
1968
1980
  tookMs: durationMs,
1969
1981
  answerTokens: tokens,
1970
1982
  answerChars: researchResult.text.length,
@@ -2328,6 +2340,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2328
2340
  conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
2329
2341
  artifacts: savedArtifacts,
2330
2342
  archive,
2343
+ modelSelection: modelSelectionEvidence,
2331
2344
  controllerPid: process.pid,
2332
2345
  };
2333
2346
  }
@@ -2389,10 +2402,14 @@ export { estimateTokenCount } from "./utils.js";
2389
2402
  export { resolveBrowserConfig, DEFAULT_BROWSER_CONFIG } from "./config.js";
2390
2403
  // biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
2391
2404
  export const __test__ = {
2405
+ assertManualLoginProfileReadyForRun,
2392
2406
  closeRemoteConnectionAfterRun,
2393
2407
  detachKeptChromeProcess,
2408
+ formatManualLoginSetupCommand,
2409
+ isManualLoginProfileInitialized,
2394
2410
  isImageOnlyUiChromeText,
2395
2411
  listIgnoredRemoteChromeFlags,
2412
+ resolveManualLoginWaitMs,
2396
2413
  shouldCloseOwnedRunTargetAfterRun,
2397
2414
  };
2398
2415
  export { syncCookies } from "./cookies.js";
@@ -0,0 +1,54 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { BrowserAutomationError } from "../oracle/errors.js";
5
+ export function resolveManualLoginWaitMs(timeoutMs, keepBrowser) {
6
+ const configured = Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
7
+ if (keepBrowser) {
8
+ return configured;
9
+ }
10
+ return Math.min(configured, 30_000);
11
+ }
12
+ export async function assertManualLoginProfileReadyForRun({ userDataDir, keepBrowser, }) {
13
+ if (keepBrowser) {
14
+ return;
15
+ }
16
+ if (await isManualLoginProfileInitialized(userDataDir)) {
17
+ return;
18
+ }
19
+ const setupCommand = formatManualLoginSetupCommand(userDataDir);
20
+ throw new BrowserAutomationError("ChatGPT browser manual-login profile is not initialized. " +
21
+ `Browser mode is using Oracle's private Chrome profile at ${userDataDir}, separate from your normal Chrome profile. ` +
22
+ `Run first-time setup, sign in there, then retry: ${setupCommand}. ` +
23
+ "If you want to reuse an already signed-in Chrome instead, use --browser-attach-running.", {
24
+ stage: "browser-login-setup",
25
+ details: {
26
+ profileDir: userDataDir,
27
+ setupCommand,
28
+ sessionStatus: "needs_login",
29
+ },
30
+ reuseProfileHint: setupCommand,
31
+ });
32
+ }
33
+ export async function isManualLoginProfileInitialized(profileDir) {
34
+ const entries = await readdir(profileDir, { withFileTypes: true }).catch(() => []);
35
+ return entries.some((entry) => {
36
+ if (!entry.name)
37
+ return false;
38
+ if (entry.name === "Default" || entry.name === "Local State")
39
+ return true;
40
+ if (entry.name.startsWith("Profile "))
41
+ return true;
42
+ return false;
43
+ });
44
+ }
45
+ export function formatManualLoginSetupCommand(profileDir) {
46
+ return [
47
+ "oracle --engine browser --browser-manual-login --browser-keep-browser",
48
+ `--browser-manual-login-profile-dir ${JSON.stringify(profileDir)}`,
49
+ '-p "HI"',
50
+ ].join(" ");
51
+ }
52
+ export function defaultManualLoginProfileDir() {
53
+ return path.join(os.homedir(), ".oracle", "browser-profile");
54
+ }
@@ -9,6 +9,7 @@ import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLe
9
9
  import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
10
10
  import { CHATGPT_URL } from "./constants.js";
11
11
  import { delay } from "./utils.js";
12
+ import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
12
13
  import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
13
14
  import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
14
15
  import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
@@ -45,13 +46,18 @@ export async function runBrowserProjectSources(request) {
45
46
  const manualLogin = Boolean(config.manualLogin);
46
47
  const manualProfileDir = config.manualLoginProfileDir
47
48
  ? path.resolve(config.manualLoginProfileDir)
48
- : path.join(os.homedir(), ".oracle", "browser-profile");
49
+ : defaultManualLoginProfileDir();
49
50
  const userDataDir = manualLogin
50
51
  ? manualProfileDir
51
52
  : await mkdtemp(path.join(os.tmpdir(), "oracle-project-sources-"));
53
+ const effectiveKeepBrowser = Boolean(config.keepBrowser);
52
54
  if (manualLogin) {
53
55
  await mkdir(userDataDir, { recursive: true });
54
56
  logger(`Manual login mode enabled; reusing persistent profile at ${userDataDir}`);
57
+ await assertManualLoginProfileReadyForRun({
58
+ userDataDir,
59
+ keepBrowser: effectiveKeepBrowser,
60
+ });
55
61
  }
56
62
  else {
57
63
  logger(`Created temporary Chrome profile at ${userDataDir}`);
@@ -73,7 +79,6 @@ export async function runBrowserProjectSources(request) {
73
79
  let removeDialogHandler = null;
74
80
  let connectionClosedUnexpectedly = false;
75
81
  let completed = false;
76
- const effectiveKeepBrowser = Boolean(config.keepBrowser);
77
82
  try {
78
83
  const acquired = manualLogin
79
84
  ? await acquireManualLoginChromeForProjectSources(userDataDir, config, logger)
@@ -140,6 +145,8 @@ export async function runBrowserProjectSources(request) {
140
145
  appliedCookies,
141
146
  manualLogin,
142
147
  timeoutMs: config.timeoutMs,
148
+ profileDir: userDataDir,
149
+ keepBrowser: effectiveKeepBrowser,
143
150
  }));
144
151
  await raceWithDisconnect(navigateToChatGPT(Page, Runtime, projectUrl, logger));
145
152
  await raceWithDisconnect(openProjectSourcesTab(Runtime, Input, config.inputTimeoutMs, logger));
@@ -261,12 +268,13 @@ async function applyProjectSourcesCookies({ config, network, manualLogin, logger
261
268
  : "No Chrome cookies found; continuing without session reuse");
262
269
  return cookieCount;
263
270
  }
264
- async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, }) {
271
+ async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, manualLogin, timeoutMs, profileDir, keepBrowser, }) {
265
272
  if (!manualLogin) {
266
273
  await ensureLoggedIn(runtime, logger, { appliedCookies });
267
274
  return;
268
275
  }
269
- const deadline = Date.now() + Math.min(timeoutMs ?? 1_200_000, 20 * 60_000);
276
+ const waitMs = resolveManualLoginWaitMs(timeoutMs, Boolean(keepBrowser));
277
+ const deadline = Date.now() + waitMs;
270
278
  let lastNotice = 0;
271
279
  while (Date.now() < deadline) {
272
280
  try {
@@ -288,7 +296,10 @@ async function waitForProjectSourcesLogin({ runtime, logger, appliedCookies, man
288
296
  await delay(1000);
289
297
  }
290
298
  }
291
- throw new Error("Manual login mode timed out waiting for ChatGPT session; please sign in and retry.");
299
+ const setupCommand = formatManualLoginSetupCommand(profileDir ?? defaultManualLoginProfileDir());
300
+ throw new Error("Manual login mode timed out waiting for ChatGPT session. " +
301
+ `Browser mode is using Oracle's private Chrome profile at ${profileDir ?? "(default profile)"}, not your normal Chrome profile. ` +
302
+ `Run first-time setup, sign in there, then retry: ${setupCommand}`);
292
303
  }
293
304
  async function acquireManualLoginChromeForProjectSources(userDataDir, config, logger) {
294
305
  const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
@@ -5,6 +5,7 @@ import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, format
5
5
  import { isKnownModel } from "../oracle/modelResolver.js";
6
6
  import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
7
7
  import { buildAttachmentPlan } from "./policies.js";
8
+ import { createStoredZip } from "./zipBundle.js";
8
9
  const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
9
10
  const MEDIA_EXTENSIONS = new Set([
10
11
  ".mp4",
@@ -34,6 +35,49 @@ export function isMediaFile(filePath) {
34
35
  const ext = path.extname(filePath).toLowerCase();
35
36
  return MEDIA_EXTENSIONS.has(ext);
36
37
  }
38
+ function formatSectionsForBundle(sections) {
39
+ const bundleLines = [];
40
+ sections.forEach((section) => {
41
+ bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
42
+ bundleLines.push("");
43
+ });
44
+ return `${bundleLines
45
+ .join("\n")
46
+ .replace(/\n{3,}/g, "\n\n")
47
+ .trimEnd()}\n`;
48
+ }
49
+ async function writeBrowserBundle(sections, format) {
50
+ const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
51
+ const tokenEstimateText = formatSectionsForBundle(sections);
52
+ if (format === "zip") {
53
+ const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
54
+ const buffer = createStoredZip(sections.map((section) => ({
55
+ path: section.displayPath,
56
+ content: section.content,
57
+ })));
58
+ await fs.writeFile(bundlePath, buffer);
59
+ return {
60
+ attachment: {
61
+ path: bundlePath,
62
+ displayPath: bundlePath,
63
+ sizeBytes: buffer.length,
64
+ },
65
+ metadata: { originalCount: sections.length, bundlePath, format },
66
+ tokenEstimateText,
67
+ };
68
+ }
69
+ const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
70
+ await fs.writeFile(bundlePath, tokenEstimateText, "utf8");
71
+ return {
72
+ attachment: {
73
+ path: bundlePath,
74
+ displayPath: bundlePath,
75
+ sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
76
+ },
77
+ metadata: { originalCount: sections.length, bundlePath, format },
78
+ tokenEstimateText,
79
+ };
80
+ }
37
81
  export async function assembleBrowserPrompt(runOptions, deps = {}) {
38
82
  const cwd = deps.cwd ?? process.cwd();
39
83
  const readFilesFn = deps.readFilesImpl ?? readFiles;
@@ -49,7 +93,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
49
93
  sizeBytes: stats.size,
50
94
  };
51
95
  }));
52
- const files = await readFilesFn(textFilePaths, { cwd });
96
+ const files = await readFilesFn(textFilePaths, {
97
+ cwd,
98
+ maxFileSizeBytes: runOptions.maxFileSizeBytes,
99
+ });
53
100
  const basePrompt = (runOptions.prompt ?? "").trim();
54
101
  const userPrompt = basePrompt;
55
102
  const systemPrompt = runOptions.system?.trim() || "";
@@ -59,6 +106,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
59
106
  ? "never"
60
107
  : (runOptions.browserAttachments ?? "auto");
61
108
  const bundleRequested = Boolean(runOptions.browserBundleFiles);
109
+ const bundleFormat = runOptions.browserBundleFormat ?? "text";
62
110
  const inlinePlan = buildAttachmentPlan(sections, { inlineFiles: true, bundleRequested });
63
111
  const uploadPlan = buildAttachmentPlan(sections, { inlineFiles: false, bundleRequested });
64
112
  const baseComposerSections = [];
@@ -88,26 +136,12 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
88
136
  let bundleText = null;
89
137
  let bundled = null;
90
138
  if (shouldBundle) {
91
- const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
92
- const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
93
- const bundleLines = [];
94
- sections.forEach((section) => {
95
- bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
96
- bundleLines.push("");
97
- });
98
- bundleText = `${bundleLines
99
- .join("\n")
100
- .replace(/\n{3,}/g, "\n\n")
101
- .trimEnd()}\n`;
102
- await fs.writeFile(bundlePath, bundleText, "utf8");
139
+ const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
140
+ bundleText = writtenBundle.tokenEstimateText;
103
141
  attachments.length = 0;
104
- attachments.push({
105
- path: bundlePath,
106
- displayPath: bundlePath,
107
- sizeBytes: Buffer.byteLength(bundleText, "utf8"),
108
- });
142
+ attachments.push(writtenBundle.attachment);
109
143
  attachments.push(...mediaAttachments);
110
- bundled = { originalCount: sections.length, bundlePath };
144
+ bundled = writtenBundle.metadata;
111
145
  }
112
146
  const inlineFileCount = selectedPlan.inlineFileCount;
113
147
  const modelConfig = isKnownModel(runOptions.model)
@@ -140,26 +174,11 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
140
174
  const fallbackAttachments = [...uploadPlan.attachments, ...mediaAttachments];
141
175
  let fallbackBundled = null;
142
176
  if (uploadPlan.shouldBundle) {
143
- const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
144
- const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
145
- const bundleLines = [];
146
- sections.forEach((section) => {
147
- bundleLines.push(formatFileSection(section.displayPath, section.content).trimEnd());
148
- bundleLines.push("");
149
- });
150
- const fallbackBundleText = `${bundleLines
151
- .join("\n")
152
- .replace(/\n{3,}/g, "\n\n")
153
- .trimEnd()}\n`;
154
- await fs.writeFile(bundlePath, fallbackBundleText, "utf8");
177
+ const writtenBundle = await writeBrowserBundle(sections, bundleFormat);
155
178
  fallbackAttachments.length = 0;
156
- fallbackAttachments.push({
157
- path: bundlePath,
158
- displayPath: bundlePath,
159
- sizeBytes: Buffer.byteLength(fallbackBundleText, "utf8"),
160
- });
179
+ fallbackAttachments.push(writtenBundle.attachment);
161
180
  fallbackAttachments.push(...mediaAttachments);
162
- fallbackBundled = { originalCount: sections.length, bundlePath };
181
+ fallbackBundled = writtenBundle.metadata;
163
182
  }
164
183
  fallback = {
165
184
  composerText: fallbackComposerText,
@@ -5,6 +5,61 @@ import { runBrowserMode } from "../browserMode.js";
5
5
  import { assembleBrowserPrompt } from "./prompt.js";
6
6
  import { BrowserAutomationError } from "../oracle/errors.js";
7
7
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
8
+ const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
9
+ const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
10
+ function buildUnavailableModelSelectionEvidence(browserConfig) {
11
+ if (!browserConfig.desiredModel) {
12
+ return undefined;
13
+ }
14
+ return {
15
+ requestedModel: browserConfig.desiredModel,
16
+ resolvedLabel: null,
17
+ strategy: browserConfig.modelStrategy,
18
+ status: "unavailable",
19
+ verified: false,
20
+ source: "config",
21
+ capturedAt: new Date().toISOString(),
22
+ };
23
+ }
24
+ function formatModelSelectionEvidence(evidence) {
25
+ const requested = evidence.requestedModel ?? "(none)";
26
+ const resolved = evidence.resolvedLabel ?? "(unavailable)";
27
+ const strategy = evidence.strategy ?? "(default)";
28
+ const verified = evidence.verified ? "yes" : "no";
29
+ return `[browser] Model selection evidence: requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}.`;
30
+ }
31
+ function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
32
+ const candidates = [
33
+ runOptions.model,
34
+ browserConfig.desiredModel,
35
+ evidence?.requestedModel,
36
+ evidence?.resolvedLabel,
37
+ ];
38
+ return candidates.some((value) => typeof value === "string" && /\bpro\b/i.test(value));
39
+ }
40
+ export function buildBrowserRunWarningsForTest(args) {
41
+ return buildBrowserRunWarnings(args);
42
+ }
43
+ function buildBrowserRunWarnings(args) {
44
+ if (!isRequestedProBrowserRun(args.runOptions, args.browserConfig, args.modelSelection) ||
45
+ args.inputTokens < LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD ||
46
+ args.elapsedMs >= LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD) {
47
+ return [];
48
+ }
49
+ return [
50
+ {
51
+ code: "browser-pro-fast-large-run",
52
+ severity: "warning",
53
+ message: `Large browser Pro run completed quickly (${(args.elapsedMs / 1000).toFixed(0)}s for ~${args.inputTokens.toLocaleString()} input tokens); verify the stored model selection evidence before claiming Pro Extended output.`,
54
+ details: {
55
+ inputTokens: args.inputTokens,
56
+ elapsedMs: args.elapsedMs,
57
+ requestedModel: args.modelSelection?.requestedModel ?? args.browserConfig.desiredModel,
58
+ resolvedLabel: args.modelSelection?.resolvedLabel ?? null,
59
+ },
60
+ },
61
+ ];
62
+ }
8
63
  export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
9
64
  const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
10
65
  const executeBrowser = deps.executeBrowser ?? runBrowserMode;
@@ -37,7 +92,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
37
92
  if (typeof message !== "string")
38
93
  return;
39
94
  const shouldAlwaysPrint = message.startsWith("[browser] ") &&
40
- /archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance/i.test(message);
95
+ /archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance|model selection|model picker/i.test(message);
41
96
  if (!runOptions.verbose && !shouldAlwaysPrint)
42
97
  return;
43
98
  log(message);
@@ -84,6 +139,20 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
84
139
  const message = error instanceof Error ? error.message : "Browser automation failed.";
85
140
  throw new BrowserAutomationError(message, { stage: "execute-browser" }, error);
86
141
  }
142
+ const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
143
+ if (modelSelection) {
144
+ log(formatModelSelectionEvidence(modelSelection));
145
+ }
146
+ const warnings = buildBrowserRunWarnings({
147
+ runOptions,
148
+ browserConfig,
149
+ inputTokens: promptArtifacts.estimatedInputTokens,
150
+ elapsedMs: browserResult.tookMs,
151
+ modelSelection,
152
+ });
153
+ for (const warning of warnings) {
154
+ log(chalk.yellow(`[browser] ${warning.message}`));
155
+ }
87
156
  if (!runOptions.silent) {
88
157
  log(chalk.bold("Answer:"));
89
158
  log(browserResult.answerMarkdown || browserResult.answerText || chalk.dim("(no text output)"));
@@ -148,6 +217,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
148
217
  controllerPid: browserResult.controllerPid ?? process.pid,
149
218
  },
150
219
  archive: browserResult.archive,
220
+ modelSelection,
221
+ warnings,
151
222
  answerText,
152
223
  artifacts: savedArtifacts,
153
224
  };