@steipete/oracle 0.16.1 → 0.17.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 (69) hide show
  1. package/README.md +72 -337
  2. package/dist/bin/oracle-cli.js +184 -55
  3. package/dist/docs-site/.nojekyll +0 -0
  4. package/dist/docs-site/CNAME +1 -0
  5. package/dist/docs-site/RELEASING.html +410 -0
  6. package/dist/docs-site/agents.html +374 -0
  7. package/dist/docs-site/anthropic.html +368 -0
  8. package/dist/docs-site/bridge.html +416 -0
  9. package/dist/docs-site/browser-mode.html +594 -0
  10. package/dist/docs-site/chromium-forks.html +347 -0
  11. package/dist/docs-site/cli-reference.html +346 -0
  12. package/dist/docs-site/configuration.html +462 -0
  13. package/dist/docs-site/favicon.svg +14 -0
  14. package/dist/docs-site/followup.html +375 -0
  15. package/dist/docs-site/gemini.html +383 -0
  16. package/dist/docs-site/grok.html +325 -0
  17. package/dist/docs-site/index.html +360 -0
  18. package/dist/docs-site/install.html +335 -0
  19. package/dist/docs-site/linux.html +321 -0
  20. package/dist/docs-site/llms.txt +43 -0
  21. package/dist/docs-site/manual-tests.html +596 -0
  22. package/dist/docs-site/mcp.html +391 -0
  23. package/dist/docs-site/multimodel.html +364 -0
  24. package/dist/docs-site/mythical-pro-agents.html +360 -0
  25. package/dist/docs-site/notifier.html +338 -0
  26. package/dist/docs-site/openai-endpoints.html +410 -0
  27. package/dist/docs-site/openrouter.html +344 -0
  28. package/dist/docs-site/quickstart.html +369 -0
  29. package/dist/docs-site/refactor/ux.html +532 -0
  30. package/dist/docs-site/sessions.html +389 -0
  31. package/dist/docs-site/social-card.png +0 -0
  32. package/dist/docs-site/social-card.svg +79 -0
  33. package/dist/docs-site/spec.html +363 -0
  34. package/dist/docs-site/testing.html +320 -0
  35. package/dist/docs-site/tui-debug.html +326 -0
  36. package/dist/docs-site/windows-work.html +324 -0
  37. package/dist/docs-site/windows.html +320 -0
  38. package/dist/scripts/test-browser.js +1 -25
  39. package/dist/src/browser/actions/modelSelection.js +36 -34
  40. package/dist/src/browser/actions/navigation.js +8 -3
  41. package/dist/src/browser/actions/thinkingTime.js +39 -3
  42. package/dist/src/browser/chromeLifecycle.js +2 -37
  43. package/dist/src/browser/index.js +23 -8
  44. package/dist/src/browser/modelDisplay.js +67 -0
  45. package/dist/src/browser/reattach.js +14 -1
  46. package/dist/src/browser/recoverConversation.js +2 -1
  47. package/dist/src/browser/sessionRunner.js +9 -10
  48. package/dist/src/browser/wslHost.js +50 -0
  49. package/dist/src/cli/browserConfig.js +31 -11
  50. package/dist/src/cli/detach.js +21 -4
  51. package/dist/src/cli/dryRun.js +13 -2
  52. package/dist/src/cli/engine.js +2 -2
  53. package/dist/src/cli/options.js +4 -0
  54. package/dist/src/cli/sessionDisplay.js +60 -8
  55. package/dist/src/cli/sessionLifecycle.js +2 -1
  56. package/dist/src/cli/sessionRunner.js +110 -60
  57. package/dist/src/cli/sessionTable.js +5 -1
  58. package/dist/src/cli/tui/index.js +12 -4
  59. package/dist/src/duration.js +3 -0
  60. package/dist/src/gemini-web/client.js +19 -1
  61. package/dist/src/oracle/modelResolver.js +8 -1
  62. package/dist/src/oracle/request.js +9 -2
  63. package/dist/src/oracle/run.js +43 -3
  64. package/dist/src/sessionManager.js +41 -12
  65. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  66. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  67. package/package.json +15 -15
  68. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  69. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -1,17 +1,14 @@
1
1
  import { rm } from "node:fs/promises";
2
- import { readFileSync } from "node:fs";
3
- import os from "node:os";
4
2
  import net from "node:net";
5
3
  import CDP from "chrome-remote-interface";
6
4
  import { launch, Launcher } from "chrome-launcher";
7
5
  import { cleanupStaleProfileState } from "./profileState.js";
8
6
  import { delay } from "./utils.js";
7
+ import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
9
8
  export async function launchChrome(config, userDataDir, logger) {
10
- const connectHost = resolveRemoteDebugHost();
11
- const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost;
9
+ const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
12
10
  const debugPort = config.debugPort ?? parseDebugPortEnv();
13
11
  const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress, config.hideWindow ?? false);
14
- const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
15
12
  // copy-profile reuses a copied signed-in profile whose cookies are
16
13
  // Keychain-encrypted, so it must launch with the real Keychain (not mocked):
17
14
  // strip the keychain-mocking flags from both chrome-launcher's defaults and
@@ -595,38 +592,6 @@ function parseDebugPortEnv() {
595
592
  }
596
593
  return value;
597
594
  }
598
- function resolveRemoteDebugHost() {
599
- const override = process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST?.trim() || process.env.WSL_HOST_IP?.trim();
600
- if (override) {
601
- return override;
602
- }
603
- if (!isWsl()) {
604
- return null;
605
- }
606
- try {
607
- const resolv = readFileSync("/etc/resolv.conf", "utf8");
608
- for (const line of resolv.split("\n")) {
609
- const match = line.match(/^nameserver\s+([0-9.]+)/);
610
- if (match?.[1]) {
611
- return match[1];
612
- }
613
- }
614
- }
615
- catch {
616
- // ignore; fall back to localhost
617
- }
618
- return null;
619
- }
620
- function isWsl() {
621
- if (process.platform !== "linux") {
622
- return false;
623
- }
624
- if (process.env.WSL_DISTRO_NAME) {
625
- return true;
626
- }
627
- const release = os.release();
628
- return release.toLowerCase().includes("microsoft");
629
- }
630
595
  async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
631
596
  const launcher = new Launcher({
632
597
  chromePath: chromePath ?? undefined,
@@ -71,6 +71,9 @@ function classifyPreservedBrowserError(error, headless) {
71
71
  function shouldPreserveBrowserOnError(error, headless) {
72
72
  return classifyPreservedBrowserError(error, headless) !== null;
73
73
  }
74
+ function normalizeAuthenticatedModelSelectionError(error) {
75
+ return error instanceof Error ? error : new Error(String(error));
76
+ }
74
77
  function shouldKeepLocalBrowserOpen(options) {
75
78
  if (options.usingCopiedProfile)
76
79
  return false;
@@ -1054,11 +1057,9 @@ export async function runBrowserMode(options) {
1054
1057
  }
1055
1058
  },
1056
1059
  })).catch((error) => {
1057
- const base = error instanceof Error ? error.message : String(error);
1058
- const hint = appliedCookies === 0
1059
- ? " No cookies were applied; log in to ChatGPT in Chrome or provide inline cookies (--browser-inline-cookies[(-file)] or ORACLE_BROWSER_COOKIES_JSON)."
1060
- : "";
1061
- throw new Error(`${base}${hint}`);
1060
+ // Login has already been verified above. Preserve the picker failure instead of
1061
+ // misdiagnosing an unavailable model as missing cookies.
1062
+ throw normalizeAuthenticatedModelSelectionError(error);
1062
1063
  });
1063
1064
  await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
1064
1065
  logger(`Prompt textarea ready (after model switch, ${promptText.length.toLocaleString()} chars queued)`);
@@ -1342,7 +1343,11 @@ export async function runBrowserMode(options) {
1342
1343
  if (conversationUrl && isConversationUrl(conversationUrl)) {
1343
1344
  logger(`[browser] Rechecking assistant response at ${conversationUrl}`);
1344
1345
  await raceWithDisconnect(Page.navigate({ url: conversationUrl }));
1345
- await raceWithDisconnect(delay(1000));
1346
+ await raceWithDisconnect(waitForResumedConversationHydration(Runtime, recheckTimeoutMs || 30_000, logger, {
1347
+ requirePriorTurns: true,
1348
+ requirePromptReady: false,
1349
+ expectedConversationUrl: conversationUrl,
1350
+ }));
1346
1351
  }
1347
1352
  // Validate session before attempting recheck - sessions can expire during the delay
1348
1353
  const sessionValid = await validateChatGPTSession(Runtime, logger);
@@ -2607,7 +2612,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
2607
2612
  lastUrl = conversationUrl;
2608
2613
  logger(`[browser] Rechecking assistant response at ${conversationUrl}`);
2609
2614
  await Page.navigate({ url: conversationUrl });
2610
- await delay(1000);
2615
+ await waitForResumedConversationHydration(Runtime, recheckTimeoutMs || 30_000, logger, {
2616
+ requirePriorTurns: true,
2617
+ requirePromptReady: false,
2618
+ expectedConversationUrl: conversationUrl,
2619
+ });
2611
2620
  }
2612
2621
  // Validate session before attempting recheck - sessions can expire during the delay
2613
2622
  const sessionValid = await validateChatGPTSession(Runtime, logger);
@@ -3051,10 +3060,12 @@ export const __test__ = {
3051
3060
  isManualLoginProfileInitialized,
3052
3061
  isImageOnlyUiChromeText,
3053
3062
  listIgnoredRemoteChromeFlags,
3063
+ normalizeAuthenticatedModelSelectionError,
3054
3064
  resolveManualLoginWaitMs,
3055
3065
  shouldCleanupBlankTabsAfterLastLease,
3056
3066
  shouldCloseOwnedRunTargetAfterRun,
3057
3067
  shouldKeepLocalBrowserOpen,
3068
+ waitForAssistantResponseWithReload,
3058
3069
  };
3059
3070
  export { syncCookies } from "./cookies.js";
3060
3071
  export { navigateToChatGPT, ensureNotBlocked, ensurePromptReady, ensureModelSelection, submitPrompt, waitForAssistantResponse, captureAssistantMarkdown, uploadAttachmentFile, waitForAttachmentCompletion, } from "./pageActions.js";
@@ -3086,7 +3097,11 @@ async function waitForAssistantResponseWithReload(Runtime, Page, timeoutMs, logg
3086
3097
  }
3087
3098
  logger("Assistant response stalled; reloading conversation and retrying once");
3088
3099
  await Page.navigate({ url: conversationUrl });
3089
- await delay(1000);
3100
+ await waitForResumedConversationHydration(Runtime, timeoutMs, logger, {
3101
+ requirePriorTurns: true,
3102
+ requirePromptReady: false,
3103
+ expectedConversationUrl: conversationUrl,
3104
+ });
3090
3105
  return await waitForAssistantResponse(Runtime, timeoutMs, logger, minTurnIndex, expectedConversationId);
3091
3106
  }
3092
3107
  }
@@ -0,0 +1,67 @@
1
+ function cleanLabel(value) {
2
+ const label = value?.trim();
3
+ return label ? label : null;
4
+ }
5
+ function sameLabel(left, right) {
6
+ return left.localeCompare(right, undefined, { sensitivity: "accent" }) === 0;
7
+ }
8
+ /**
9
+ * Describe what a browser run will try to select without presenting the target as observed fact.
10
+ */
11
+ export function formatBrowserModelTarget({ model, desiredModel, modelStrategy, }) {
12
+ const requested = cleanLabel(model) ?? "n/a";
13
+ if (modelStrategy === "current" || modelStrategy === "ignore") {
14
+ return `picker=${modelStrategy}; requested=${requested}`;
15
+ }
16
+ const target = cleanLabel(desiredModel);
17
+ if (!target) {
18
+ return requested;
19
+ }
20
+ return `target=${target}; requested=${requested}`;
21
+ }
22
+ /**
23
+ * Prefer picker evidence only when Oracle verified it. Otherwise retain the requested CLI key.
24
+ * In particular, a bare `Pro` picker label must not be expanded to a server-side model version.
25
+ */
26
+ export function resolveBrowserModelDisplayName({ model, evidence, }) {
27
+ const verifiedLabel = evidence?.verified ? cleanLabel(evidence.resolvedLabel) : null;
28
+ return verifiedLabel ?? cleanLabel(model) ?? "n/a";
29
+ }
30
+ export function formatBrowserModelWithRequestedKey(input) {
31
+ const displayName = resolveBrowserModelDisplayName(input);
32
+ const requested = cleanLabel(input.model);
33
+ if (!requested || sameLabel(displayName, requested)) {
34
+ return displayName;
35
+ }
36
+ return `${displayName} (requested ${requested})`;
37
+ }
38
+ export function resolveSessionBrowserModelDisplayName(metadata, model = metadata.model) {
39
+ const sessionModel = cleanLabel(metadata.model);
40
+ const requestedModel = cleanLabel(model);
41
+ const evidenceApplies = requestedModel === null
42
+ ? sessionModel === null
43
+ : sessionModel !== null && sameLabel(requestedModel, sessionModel);
44
+ return resolveBrowserModelDisplayName({
45
+ model,
46
+ evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
47
+ });
48
+ }
49
+ export function formatSessionBrowserModelWithRequestedKey(metadata, model = metadata.model) {
50
+ const sessionModel = cleanLabel(metadata.model);
51
+ const requestedModel = cleanLabel(model);
52
+ const evidenceApplies = requestedModel === null
53
+ ? sessionModel === null
54
+ : sessionModel !== null && sameLabel(requestedModel, sessionModel);
55
+ return formatBrowserModelWithRequestedKey({
56
+ model,
57
+ evidence: evidenceApplies ? metadata.browser?.modelSelection : undefined,
58
+ });
59
+ }
60
+ export function formatBrowserModelSelectionEvidence(evidence, model) {
61
+ const requestedKey = cleanLabel(model) ?? "(none)";
62
+ const target = cleanLabel(evidence.requestedModel) ?? "(none)";
63
+ const resolvedLabel = cleanLabel(evidence.resolvedLabel) ?? "(unavailable)";
64
+ const strategy = evidence.strategy ?? "(default)";
65
+ const verified = evidence.verified ? "yes" : "no";
66
+ return `requestedKey=${requestedKey}; target=${target}; resolvedLabel=${resolvedLabel}; status=${evidence.status}; strategy=${strategy}; verified=${verified}; source=${evidence.source}; capturedAt=${evidence.capturedAt}`;
67
+ }
@@ -2,7 +2,7 @@ import CDP from "chrome-remote-interface";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { mkdtemp, mkdir, rm } from "node:fs/promises";
5
- import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
5
+ import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, } from "./pageActions.js";
6
6
  import { launchChrome, connectToChrome, positionChromeWindowOffscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
7
7
  import { resolveBrowserConfig } from "./config.js";
8
8
  import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
@@ -98,6 +98,13 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
98
98
  const pingTimeoutMs = Math.min(5_000, Math.max(1_500, Math.floor(timeoutMs * 0.05)));
99
99
  await withTimeout(Runtime.evaluate({ expression: "1+1", returnByValue: true }), pingTimeoutMs, "Reattach target did not respond");
100
100
  await ensureConversationOpen();
101
+ const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
102
+ const expectedConversationUrl = buildConversationUrl(runtime, resolveBrowserConfig(config ?? {}).url);
103
+ await waitForHydration(Runtime, timeoutMs, logger, {
104
+ requirePriorTurns: true,
105
+ requirePromptReady: false,
106
+ expectedConversationUrl: expectedConversationUrl ?? undefined,
107
+ });
101
108
  const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
102
109
  (deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
103
110
  if (config?.researchMode === "deep") {
@@ -226,6 +233,12 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
226
233
  }
227
234
  await waitForLocationChange(Runtime, 15_000);
228
235
  }
236
+ const waitForHydration = deps.waitForConversationHydration ?? waitForResumedConversationHydration;
237
+ await waitForHydration(Runtime, resolved.inputTimeoutMs, logger, {
238
+ requirePriorTurns: true,
239
+ requirePromptReady: false,
240
+ expectedConversationUrl: conversationUrl ?? undefined,
241
+ });
229
242
  const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
230
243
  const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
231
244
  const timeoutMs = resolved.timeoutMs ?? 120_000;
@@ -67,7 +67,8 @@ export function isRecoveredConversationHarvestReady(harvested) {
67
67
  (typeof harvested.lastAssistantTurnIndex === "number" &&
68
68
  typeof harvested.lastUserTurnIndex === "number" &&
69
69
  harvested.lastAssistantTurnIndex > harvested.lastUserTurnIndex);
70
- return (harvested.stopExists === true ||
70
+ const hasHydratedUserTurn = typeof harvested.lastUserTurnIndex === "number" && harvested.lastUserTurnIndex >= 0;
71
+ return ((harvested.stopExists === true && hasHydratedUserTurn) ||
71
72
  ((harvested.assistantCount ?? 0) > 0 &&
72
73
  assistantFollowsLatestUser &&
73
74
  latestAssistant.trim().length > 0 &&
@@ -5,6 +5,7 @@ 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
+ import { formatBrowserModelSelectionEvidence, formatBrowserModelTarget, resolveBrowserModelDisplayName, } from "./modelDisplay.js";
8
9
  const LARGE_PRO_FAST_INPUT_TOKEN_THRESHOLD = 25_000;
9
10
  const LARGE_PRO_FAST_ELAPSED_MS_THRESHOLD = 120_000;
10
11
  function buildUnavailableModelSelectionEvidence(browserConfig) {
@@ -21,13 +22,6 @@ function buildUnavailableModelSelectionEvidence(browserConfig) {
21
22
  capturedAt: new Date().toISOString(),
22
23
  };
23
24
  }
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
25
  function isRequestedProBrowserRun(runOptions, browserConfig, evidence) {
32
26
  const candidates = [
33
27
  runOptions.model,
@@ -87,7 +81,12 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
87
81
  if (promptArtifacts.bundled) {
88
82
  log(chalk.dim(`Packed ${promptArtifacts.bundled.originalCount} files into 1 bundle (contents counted in token estimate).`));
89
83
  }
90
- const headerLine = `Launching browser mode (${runOptions.model}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
84
+ const launchModel = formatBrowserModelTarget({
85
+ model: runOptions.model,
86
+ desiredModel: browserConfig.desiredModel,
87
+ modelStrategy: browserConfig.modelStrategy,
88
+ });
89
+ const headerLine = `Launching browser mode (${launchModel}) with ~${promptArtifacts.estimatedInputTokens.toLocaleString()} tokens.`;
91
90
  const automationLogger = ((message) => {
92
91
  if (typeof message !== "string")
93
92
  return;
@@ -150,7 +149,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
150
149
  }
151
150
  const modelSelection = browserResult.modelSelection ?? buildUnavailableModelSelectionEvidence(browserConfig);
152
151
  if (modelSelection) {
153
- log(formatModelSelectionEvidence(modelSelection));
152
+ log(`[browser] Model selection evidence: ${formatBrowserModelSelectionEvidence(modelSelection, runOptions.model)}`);
154
153
  }
155
154
  const warnings = buildBrowserRunWarnings({
156
155
  runOptions,
@@ -199,7 +198,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
199
198
  })();
200
199
  const { line1, line2 } = formatFinishLine({
201
200
  elapsedMs: browserResult.tookMs,
202
- model: `${runOptions.model}[browser]`,
201
+ model: `${resolveBrowserModelDisplayName({ model: runOptions.model, evidence: modelSelection })}[browser]`,
203
202
  tokensPart,
204
203
  detailParts: [
205
204
  runOptions.file && runOptions.file.length > 0 ? `files=${runOptions.file.length}` : null,
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ export function isWsl() {
4
+ if (process.platform !== "linux")
5
+ return false;
6
+ if (process.env.WSL_DISTRO_NAME)
7
+ return true;
8
+ return os.release().toLowerCase().includes("microsoft");
9
+ }
10
+ export function parseWslResolverHost(resolvConf) {
11
+ for (const line of resolvConf.split("\n")) {
12
+ const match = line.match(/^nameserver\s+([0-9.]+)/);
13
+ if (match?.[1]) {
14
+ return match[1].startsWith("127.") ? "127.0.0.1" : match[1];
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ export function resolveWslHost() {
20
+ if (!isWsl())
21
+ return null;
22
+ try {
23
+ return parseWslResolverHost(readFileSync("/etc/resolv.conf", "utf8"));
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export function resolveWslChromeHost(options = {}) {
30
+ const remoteDebugHost = options.remoteDebugHost === undefined
31
+ ? process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST
32
+ : options.remoteDebugHost;
33
+ const wslHostIp = options.wslHostIp === undefined ? process.env.WSL_HOST_IP : options.wslHostIp;
34
+ const override = remoteDebugHost?.trim() || wslHostIp?.trim();
35
+ if (override)
36
+ return override;
37
+ if (options.resolvConf !== undefined) {
38
+ return options.resolvConf === null ? null : parseWslResolverHost(options.resolvConf);
39
+ }
40
+ return resolveWslHost();
41
+ }
42
+ export function resolveWslChromeLaunchRoute(options = {}) {
43
+ const connectHost = resolveWslChromeHost(options);
44
+ const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
45
+ return {
46
+ connectHost,
47
+ debugBindAddress: usePatchedLauncher ? "0.0.0.0" : connectHost,
48
+ usePatchedLauncher,
49
+ };
50
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import chalk from "chalk";
3
4
  import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
4
5
  import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
5
6
  import { normalizeChatgptUrl } from "../browser/utils.js";
@@ -86,6 +87,7 @@ export async function buildBrowserConfig(options) {
86
87
  const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
87
88
  const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
88
89
  const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
90
+ assertBrowserModelAvailable(options.model, modelStrategy);
89
91
  const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
90
92
  let inline = await resolveInlineCookies({
91
93
  inlineArg: options.browserInlineCookies,
@@ -123,38 +125,38 @@ export async function buildBrowserConfig(options) {
123
125
  url,
124
126
  debugPort: selectBrowserPort(options),
125
127
  timeoutMs: options.browserTimeout
126
- ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS)
128
+ ? parseBrowserDuration(options.browserTimeout, "--browser-timeout", DEFAULT_BROWSER_TIMEOUT_MS)
127
129
  : undefined,
128
130
  inputTimeoutMs: options.browserInputTimeout
129
- ? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
131
+ ? parseBrowserDuration(options.browserInputTimeout, "--browser-input-timeout", DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
130
132
  : undefined,
131
133
  attachmentTimeoutMs: options.browserAttachmentTimeout
132
- ? parseDuration(options.browserAttachmentTimeout, DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
134
+ ? parseBrowserDuration(options.browserAttachmentTimeout, "--browser-attachment-timeout", DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
133
135
  : undefined,
134
136
  assistantRecheckDelayMs: options.browserRecheckDelay
135
- ? parseDuration(options.browserRecheckDelay, 0)
137
+ ? parseBrowserDuration(options.browserRecheckDelay, "--browser-recheck-delay", 0)
136
138
  : undefined,
137
139
  assistantRecheckTimeoutMs: options.browserRecheckTimeout
138
- ? parseDuration(options.browserRecheckTimeout, DEFAULT_BROWSER_RECHECK_TIMEOUT_MS)
140
+ ? parseBrowserDuration(options.browserRecheckTimeout, "--browser-recheck-timeout", DEFAULT_BROWSER_RECHECK_TIMEOUT_MS)
139
141
  : undefined,
140
142
  reuseChromeWaitMs: options.browserReuseWait
141
- ? parseDuration(options.browserReuseWait, 0)
143
+ ? parseBrowserDuration(options.browserReuseWait, "--browser-reuse-wait", 0)
142
144
  : undefined,
143
145
  profileLockTimeoutMs: options.browserProfileLockTimeout
144
- ? parseDuration(options.browserProfileLockTimeout, 0)
146
+ ? parseBrowserDuration(options.browserProfileLockTimeout, "--browser-profile-lock-timeout", 0)
145
147
  : undefined,
146
148
  maxConcurrentTabs: parseMaxConcurrentTabs(options.browserMaxConcurrentTabs),
147
149
  autoReattachDelayMs: options.browserAutoReattachDelay
148
- ? parseDuration(options.browserAutoReattachDelay, 0)
150
+ ? parseBrowserDuration(options.browserAutoReattachDelay, "--browser-auto-reattach-delay", 0)
149
151
  : undefined,
150
152
  autoReattachIntervalMs: options.browserAutoReattachInterval
151
- ? parseDuration(options.browserAutoReattachInterval, 0)
153
+ ? parseBrowserDuration(options.browserAutoReattachInterval, "--browser-auto-reattach-interval", 0)
152
154
  : undefined,
153
155
  autoReattachTimeoutMs: options.browserAutoReattachTimeout
154
- ? parseDuration(options.browserAutoReattachTimeout, DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS)
156
+ ? parseBrowserDuration(options.browserAutoReattachTimeout, "--browser-auto-reattach-timeout", DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS)
155
157
  : undefined,
156
158
  cookieSyncWaitMs: options.browserCookieWait
157
- ? parseDuration(options.browserCookieWait, 0)
159
+ ? parseBrowserDuration(options.browserCookieWait, "--browser-cookie-wait", 0)
158
160
  : undefined,
159
161
  cookieSync: options.browserNoCookieSync ? false : undefined,
160
162
  cookieNames,
@@ -178,6 +180,17 @@ export async function buildBrowserConfig(options) {
178
180
  archiveConversations: options.browserArchive,
179
181
  };
180
182
  }
183
+ function assertBrowserModelAvailable(model, modelStrategy) {
184
+ if (modelStrategy !== "select")
185
+ return;
186
+ const normalized = normalizeChatGptModelForBrowser(model);
187
+ if (normalized !== "gpt-5.2" &&
188
+ normalized !== "gpt-5.2-instant" &&
189
+ normalized !== "gpt-5.2-thinking") {
190
+ return;
191
+ }
192
+ throw new Error(`Browser model "${model}" is retired because ChatGPT no longer offers GPT-5.2 base, Instant, or Thinking. Choose a current GPT-5.5/GPT-5.6 browser model, use --browser-model-strategy current to keep ChatGPT's active model, or use --engine api to retain the GPT-5.2 API alias.`);
193
+ }
181
194
  function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies, }) {
182
195
  if (!attachRunning) {
183
196
  return;
@@ -218,6 +231,13 @@ function parseMaxConcurrentTabs(raw) {
218
231
  }
219
232
  return Math.trunc(value);
220
233
  }
234
+ function parseBrowserDuration(raw, optionName, fallbackMs) {
235
+ const parsed = parseDuration(raw, Number.NaN);
236
+ if (Number.isFinite(parsed))
237
+ return parsed;
238
+ console.log(chalk.yellow(`Warning: invalid ${optionName} duration "${raw}"; using fallback ${fallbackMs}ms.`));
239
+ return fallbackMs;
240
+ }
221
241
  export function mapModelToBrowserLabel(model) {
222
242
  const normalized = normalizeChatGptModelForBrowser(model);
223
243
  // Iterate ordered array to find first match (most specific first)
@@ -1,14 +1,31 @@
1
1
  import { isProModel } from "../oracle/modelResolver.js";
2
2
  export function shouldDetachSession({
3
3
  // Params kept for policy tweaks.
4
- engine, model, waitPreference, disableDetachEnv, }) {
4
+ engine, model, reasoningMode, waitPreference, disableDetachEnv, }) {
5
5
  if (disableDetachEnv)
6
6
  return false;
7
- // Explicit --wait means "stay attached", regardless of model defaults.
7
+ // Keep long local browser Pro work in a separate process even while the CLI
8
+ // stays attached to its session log. If the foreground stream is interrupted,
9
+ // the worker can still finish the browser run and persist the answer.
10
+ if (engine === "browser" && isProModel(model))
11
+ return true;
12
+ // For API runs, explicit --wait keeps execution in the foreground.
8
13
  if (waitPreference)
9
14
  return false;
10
- // Only Pro-tier API runs should start detached by default; browser runs stay inline so failures surface.
11
- if (isProModel(model) && engine === "api")
15
+ // Pro-tier API runs start detached by default.
16
+ if ((isProModel(model) || reasoningMode === "pro") && engine === "api")
12
17
  return true;
13
18
  return false;
14
19
  }
20
+ export function stopDetachedWorker(workerPid, kill = process.kill) {
21
+ try {
22
+ kill(workerPid, "SIGTERM");
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ if (error && typeof error === "object" && "code" in error && error.code === "ESRCH") {
27
+ return false;
28
+ }
29
+ throw error;
30
+ }
31
+ }
@@ -5,6 +5,7 @@ import { assembleBrowserPrompt } from "../browser/prompt.js";
5
5
  import { buildTokenEstimateSuffix, formatAttachmentLabel } from "../browser/promptSummary.js";
6
6
  import { buildCookiePlan } from "../browser/policies.js";
7
7
  import { describeBrowserControlPlan, formatBrowserControlPlan } from "../browser/controlPlan.js";
8
+ import { formatBrowserModelTarget } from "../browser/modelDisplay.js";
8
9
  export async function runDryRunSummary({ engine, runOptions, cwd, version, log, browserConfig, }, deps = {}) {
9
10
  if (engine === "browser") {
10
11
  await runBrowserDryRun({ runOptions, cwd, version, log, browserConfig }, deps);
@@ -45,7 +46,12 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig,
45
46
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
46
47
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
47
48
  const suffix = buildTokenEstimateSuffix(artifacts);
48
- const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
49
+ const displayModel = formatBrowserModelTarget({
50
+ model: runOptions.model,
51
+ desiredModel: browserConfig?.desiredModel,
52
+ modelStrategy: browserConfig?.modelStrategy,
53
+ });
54
+ const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
49
55
  log(chalk.cyan(headerLine));
50
56
  logBrowserControlPlan(browserConfig, log, "dry-run");
51
57
  logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
@@ -95,7 +101,12 @@ export async function runBrowserPreview({ runOptions, cwd, version, previewMode,
95
101
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
96
102
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
97
103
  const suffix = buildTokenEstimateSuffix(artifacts);
98
- const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
104
+ const displayModel = formatBrowserModelTarget({
105
+ model: runOptions.model,
106
+ desiredModel: browserConfig?.desiredModel,
107
+ modelStrategy: browserConfig?.modelStrategy,
108
+ });
109
+ const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
99
110
  log(chalk.cyan(headerLine));
100
111
  logBrowserControlPlan(browserConfig, log, "preview");
101
112
  logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
@@ -1,7 +1,7 @@
1
1
  import { isProModel } from "../oracle/modelResolver.js";
2
- export function defaultWaitPreference(model, engine) {
2
+ export function defaultWaitPreference(model, engine, reasoningMode) {
3
3
  // Pro-class API runs can take a long time; prefer non-blocking unless explicitly overridden.
4
- if (engine === "api" && isProModel(model)) {
4
+ if (engine === "api" && (isProModel(model) || reasoningMode === "pro")) {
5
5
  return false;
6
6
  }
7
7
  return true; // browser or non-pro models are fast enough to block by default
@@ -187,6 +187,10 @@ export function resolveApiModel(modelValue) {
187
187
  if (normalized.includes("/")) {
188
188
  return normalized;
189
189
  }
190
+ const gpt56Label = parseBrowserGpt56Label(normalized);
191
+ if (gpt56Label?.variant.split(" ").includes("pro")) {
192
+ throw new InvalidArgumentError("GPT-5.6 Pro is an API reasoning mode, not a model slug. Use --model gpt-5.6-sol --reasoning-mode pro.");
193
+ }
190
194
  if (normalized.includes("grok")) {
191
195
  return "grok-4.1";
192
196
  }