@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
@@ -12,6 +12,7 @@ import { estimateTokenCount } from "../browser/utils.js";
12
12
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
13
13
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
14
14
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
15
+ import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
15
16
  const isTty = () => Boolean(process.stdout.isTTY);
16
17
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
17
18
  export const MAX_RENDER_BYTES = 200_000;
@@ -185,6 +186,7 @@ export async function attachSession(sessionId, options) {
185
186
  const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
186
187
  const runtime = metadata.browser?.runtime;
187
188
  const controllerAlive = isProcessAlive(runtime?.controllerPid);
189
+ const workerAlive = isProcessAlive(metadata.lifecycle?.workerPid);
188
190
  const hasChromeDisconnect = metadata.response?.incompleteReason === "chrome-disconnected";
189
191
  const hasIncompleteCapture = metadata.response?.incompleteReason === "incomplete-capture";
190
192
  const statusAllowsReattach = metadata.status === "running" ||
@@ -204,6 +206,7 @@ export async function attachSession(sessionId, options) {
204
206
  const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
205
207
  metadata.mode === "browser" &&
206
208
  hasFallbackSessionInfo &&
209
+ !workerAlive &&
207
210
  (hasRecoverableConversation ||
208
211
  runtime?.promptSubmitted ||
209
212
  hasLiveChromeFallback ||
@@ -311,11 +314,17 @@ export async function attachSession(sessionId, options) {
311
314
  const usage = run.usage
312
315
  ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
313
316
  : "";
314
- console.log(`- ${chalk.cyan(run.model)} ${run.status}${usage}`);
317
+ const modelLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
318
+ ? formatSessionBrowserModelWithRequestedKey(metadata, run.model)
319
+ : run.model;
320
+ console.log(`- ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
315
321
  }
316
322
  }
317
323
  else if (metadata.model) {
318
- console.log(`Model: ${metadata.model}`);
324
+ const modelLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
325
+ ? formatSessionBrowserModelWithRequestedKey(metadata)
326
+ : metadata.model;
327
+ console.log(`Model: ${modelLabel}`);
319
328
  }
320
329
  const browserEvidence = formatBrowserEvidence(metadata);
321
330
  if (browserEvidence) {
@@ -391,6 +400,9 @@ export async function attachSession(sessionId, options) {
391
400
  if (summary) {
392
401
  console.log(`\n${chalk.green.bold(summary)}`);
393
402
  }
403
+ if (options?.propagateFailure && metadata.status === "error") {
404
+ process.exitCode = 1;
405
+ }
394
406
  return;
395
407
  }
396
408
  if (wantsRender) {
@@ -493,6 +505,48 @@ export async function attachSession(sessionId, options) {
493
505
  }
494
506
  }
495
507
  }
508
+ if (options?.propagateFailure && latest.status === "error") {
509
+ process.exitCode = 1;
510
+ }
511
+ break;
512
+ }
513
+ const controllerPid = latest.lifecycle?.workerPid ?? latest.browser?.runtime?.controllerPid;
514
+ if (latest.lifecycle?.detached && controllerPid && !isProcessAlive(controllerPid)) {
515
+ const settled = await sessionStore.readSession(sessionId);
516
+ if (!settled) {
517
+ break;
518
+ }
519
+ if (settled.status === "completed" || settled.status === "partial") {
520
+ continue;
521
+ }
522
+ await printNew();
523
+ flushRemainder();
524
+ const message = settled.status === "error"
525
+ ? (settled.errorMessage ?? "Detached worker failed.")
526
+ : "Detached worker exited before the session reached a terminal state.";
527
+ const failure = {
528
+ category: "internal",
529
+ message,
530
+ };
531
+ if (settled.model) {
532
+ await sessionStore.updateModelRun(settled.id, settled.model, {
533
+ status: "error",
534
+ completedAt: new Date().toISOString(),
535
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
536
+ error: failure,
537
+ });
538
+ }
539
+ await sessionStore.updateSession(settled.id, {
540
+ status: "error",
541
+ completedAt: new Date().toISOString(),
542
+ errorMessage: message,
543
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
544
+ error: failure,
545
+ });
546
+ console.log(chalk.yellow(`${message} Reattach via: ${settled.lifecycle?.reattachCommand}`));
547
+ if (options?.propagateFailure) {
548
+ process.exitCode = 1;
549
+ }
496
550
  break;
497
551
  }
498
552
  await wait(1000);
@@ -555,11 +609,7 @@ export function formatBrowserEvidence(metadata) {
555
609
  const lines = [];
556
610
  const evidence = browser.modelSelection;
557
611
  if (evidence) {
558
- const requested = evidence.requestedModel ?? "(none)";
559
- const resolved = evidence.resolvedLabel ?? "(unavailable)";
560
- const strategy = evidence.strategy ?? "(default)";
561
- const verified = evidence.verified ? "yes" : "no";
562
- lines.push(`model requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}`);
612
+ lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
563
613
  }
564
614
  for (const warning of browser.warnings ?? []) {
565
615
  lines.push(`warning ${warning.code}: ${warning.message}`);
@@ -821,7 +871,9 @@ export function formatCompletionSummary(metadata, options = {}) {
821
871
  if (!metadata.usage || metadata.elapsedMs == null) {
822
872
  return null;
823
873
  }
824
- const modeLabel = metadata.mode === "browser" ? `${metadata.model ?? "n/a"}[browser]` : (metadata.model ?? "n/a");
874
+ const modeLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
875
+ ? `${resolveSessionBrowserModelDisplayName(metadata)}[browser]`
876
+ : (metadata.model ?? "n/a");
825
877
  const usage = metadata.usage;
826
878
  const cost = resolveSessionCost(metadata);
827
879
  const tokensDisplay = [
@@ -1,9 +1,10 @@
1
- export function buildSessionLifecycle({ engine, detached, reattachCommand, }) {
1
+ export function buildSessionLifecycle({ engine, detached, workerPid, reattachCommand, }) {
2
2
  return {
3
3
  engine,
4
4
  execution: detached ? "background" : "foreground",
5
5
  attached: !detached,
6
6
  detached,
7
+ workerPid,
7
8
  reattachCommand,
8
9
  };
9
10
  }
@@ -78,6 +78,15 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
78
78
  cwd,
79
79
  log,
80
80
  }, runnerDeps);
81
+ await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
82
+ await sendSessionNotification({
83
+ sessionId: sessionMeta.id,
84
+ sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
85
+ mode,
86
+ model: sessionMeta.model,
87
+ usage: result.usage,
88
+ characters: result.answerText?.length,
89
+ }, notificationSettings, log, result.answerText?.slice(0, 140));
81
90
  if (modelForStatus) {
82
91
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
83
92
  status: "completed",
@@ -103,15 +112,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
103
112
  transport: undefined,
104
113
  error: undefined,
105
114
  });
106
- await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
107
- await sendSessionNotification({
108
- sessionId: sessionMeta.id,
109
- sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
110
- mode,
111
- model: sessionMeta.model,
112
- usage: result.usage,
113
- characters: result.answerText?.length,
114
- }, notificationSettings, log, result.answerText?.slice(0, 140));
115
115
  return;
116
116
  }
117
117
  const multiModels = Array.isArray(runOptions.models) ? runOptions.models.filter(Boolean) : [];
@@ -362,16 +362,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
362
362
  if (result.mode !== "live") {
363
363
  throw new Error("Unexpected preview result while running a session.");
364
364
  }
365
- await sessionStore.updateSession(sessionMeta.id, {
366
- status: "completed",
367
- completedAt: new Date().toISOString(),
368
- usage: result.usage,
369
- elapsedMs: result.elapsedMs,
370
- errorMessage: undefined,
371
- response: extractResponseMetadata(result.response),
372
- transport: undefined,
373
- error: undefined,
374
- });
375
365
  if (modelForStatus && singleModelOverride == null) {
376
366
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
377
367
  status: "completed",
@@ -389,6 +379,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
389
379
  usage: result.usage,
390
380
  characters: answerText.length,
391
381
  }, notificationSettings, log, answerText.slice(0, 140));
382
+ await sessionStore.updateSession(sessionMeta.id, {
383
+ status: "completed",
384
+ completedAt: new Date().toISOString(),
385
+ usage: result.usage,
386
+ elapsedMs: result.elapsedMs,
387
+ errorMessage: undefined,
388
+ response: extractResponseMetadata(result.response),
389
+ transport: undefined,
390
+ error: undefined,
391
+ });
392
392
  }
393
393
  catch (error) {
394
394
  const message = formatError(error);
@@ -510,16 +510,60 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
510
510
  const runtime = userError.details
511
511
  ?.runtime;
512
512
  log(dim("Assistant response timed out; marking capture incomplete for reattach."));
513
+ const timeoutResponse = {
514
+ status: "incomplete",
515
+ incompleteReason: "incomplete-capture",
516
+ };
517
+ const timeoutError = {
518
+ category: userError.category,
519
+ message: userError.message,
520
+ details: userError.details,
521
+ };
522
+ const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
523
+ const autoRuntime = runtime ?? currentBrowser?.runtime;
524
+ const willAutoReattach = autoReattachIntervalMs > 0 && Boolean(autoRuntime);
525
+ if (willAutoReattach) {
526
+ if (modelForStatus) {
527
+ await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
528
+ status: "running",
529
+ completedAt: undefined,
530
+ response: timeoutResponse,
531
+ error: timeoutError,
532
+ });
533
+ }
534
+ await sessionStore.updateSession(sessionMeta.id, {
535
+ status: "running",
536
+ completedAt: undefined,
537
+ errorMessage: message,
538
+ mode,
539
+ browser: {
540
+ ...currentBrowser,
541
+ config: browserConfig,
542
+ runtime: autoRuntime,
543
+ },
544
+ response: timeoutResponse,
545
+ error: timeoutError,
546
+ });
547
+ const success = await autoReattachUntilComplete({
548
+ sessionMeta,
549
+ runtime: autoRuntime,
550
+ browserConfig,
551
+ browserMetadata: currentBrowser,
552
+ runOptions,
553
+ modelForStatus,
554
+ notificationSettings,
555
+ log,
556
+ });
557
+ if (success) {
558
+ return;
559
+ }
560
+ }
513
561
  if (modelForStatus) {
514
562
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
515
563
  status: "error",
516
564
  completedAt: new Date().toISOString(),
517
- response: { status: "incomplete", incompleteReason: "incomplete-capture" },
518
- error: {
519
- category: userError.category,
520
- message: userError.message,
521
- details: userError.details,
522
- },
565
+ response: timeoutResponse,
566
+ error: timeoutError,
523
567
  });
524
568
  }
525
569
  await sessionStore.updateSession(sessionMeta.id, {
@@ -532,30 +576,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
532
576
  config: browserConfig,
533
577
  runtime: runtime ?? currentBrowser?.runtime,
534
578
  },
535
- response: { status: "incomplete", incompleteReason: "incomplete-capture" },
536
- error: {
537
- category: userError.category,
538
- message: userError.message,
539
- details: userError.details,
540
- },
579
+ response: timeoutResponse,
580
+ error: timeoutError,
541
581
  });
542
- const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
543
- if (autoReattachIntervalMs > 0) {
544
- const autoRuntime = runtime ?? currentBrowser?.runtime;
545
- const success = await autoReattachUntilComplete({
546
- sessionMeta,
547
- runtime: autoRuntime ?? undefined,
548
- browserConfig,
549
- browserMetadata: currentBrowser,
550
- runOptions,
551
- modelForStatus,
552
- notificationSettings,
553
- log,
554
- });
555
- if (success) {
556
- return;
557
- }
558
- }
559
582
  logBrowserReattachGuidance(runtime ?? currentBrowser?.runtime);
560
583
  return;
561
584
  }
@@ -911,6 +934,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
911
934
  }
912
935
  attempt += 1;
913
936
  log(dim(`Auto-reattach attempt ${attempt}...`));
937
+ let captureSucceeded = false;
914
938
  try {
915
939
  const reattachConfig = {
916
940
  ...browserConfig,
@@ -919,6 +943,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
919
943
  const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
920
944
  promptPreview: sessionMeta.promptPreview,
921
945
  });
946
+ captureSucceeded = true;
922
947
  const answerText = result.answerMarkdown || result.answerText || "";
923
948
  const outputTokens = estimateTokenCount(answerText);
924
949
  const artifacts = await ensureSessionArtifacts({
@@ -947,6 +972,18 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
947
972
  },
948
973
  });
949
974
  }
975
+ await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
976
+ await sendSessionNotification({
977
+ sessionId: sessionMeta.id,
978
+ sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
979
+ mode: sessionMeta.mode ?? "browser",
980
+ model: sessionMeta.model ?? runOptions.model,
981
+ usage: {
982
+ inputTokens: 0,
983
+ outputTokens,
984
+ },
985
+ characters: answerText.length,
986
+ }, notificationSettings, log, answerText.slice(0, 140));
950
987
  await sessionStore.updateSession(sessionMeta.id, {
951
988
  status: "completed",
952
989
  completedAt: new Date().toISOString(),
@@ -967,22 +1004,35 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
967
1004
  error: undefined,
968
1005
  transport: undefined,
969
1006
  });
970
- await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
971
- await sendSessionNotification({
972
- sessionId: sessionMeta.id,
973
- sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
974
- mode: sessionMeta.mode ?? "browser",
975
- model: sessionMeta.model ?? runOptions.model,
976
- usage: {
977
- inputTokens: 0,
978
- outputTokens,
979
- },
980
- characters: answerText.length,
981
- }, notificationSettings, log, answerText.slice(0, 140));
982
1007
  log(kleur.green("Auto-reattach succeeded; session marked completed."));
983
1008
  return true;
984
1009
  }
985
1010
  catch (error) {
1011
+ if (captureSucceeded) {
1012
+ const message = formatError(error);
1013
+ if (modelForStatus) {
1014
+ await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
1015
+ status: "error",
1016
+ completedAt: new Date().toISOString(),
1017
+ });
1018
+ }
1019
+ await sessionStore.updateSession(sessionMeta.id, {
1020
+ status: "error",
1021
+ completedAt: new Date().toISOString(),
1022
+ errorMessage: message,
1023
+ browser: {
1024
+ ...browserMetadata,
1025
+ config: browserConfig,
1026
+ runtime,
1027
+ },
1028
+ response: { status: "error", incompleteReason: "incomplete-capture" },
1029
+ error: {
1030
+ category: "internal",
1031
+ message,
1032
+ },
1033
+ });
1034
+ throw error;
1035
+ }
986
1036
  const message = error instanceof Error ? error.message : String(error);
987
1037
  log(dim(`Auto-reattach attempt ${attempt} failed: ${message}`));
988
1038
  }
@@ -3,6 +3,7 @@ import kleur from "kleur";
3
3
  import { MODEL_CONFIGS } from "../oracle.js";
4
4
  import { estimateUsdCost } from "tokentally";
5
5
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
6
+ import { resolveSessionBrowserModelDisplayName } from "../browser/modelDisplay.js";
6
7
  const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
7
8
  const dim = (text, rich) => (rich ? kleur.dim(text) : text);
8
9
  export const STATUS_PAD = 9;
@@ -18,7 +19,10 @@ export function formatSessionTableHeader(rich) {
18
19
  export function formatSessionTableRow(meta, options) {
19
20
  const rich = isRich(options?.rich);
20
21
  const status = colorStatus(meta.status ?? "unknown", rich);
21
- const modelLabel = (meta.model ?? "n/a").padEnd(MODEL_PAD);
22
+ const displayModel = (meta.mode ?? meta.options?.mode) === "browser"
23
+ ? resolveSessionBrowserModelDisplayName(meta)
24
+ : (meta.model ?? "n/a");
25
+ const modelLabel = displayModel.padEnd(MODEL_PAD);
22
26
  const model = rich ? chalk.white(modelLabel) : modelLabel;
23
27
  const modeLabel = formatSessionExecutionLabel(meta).padEnd(MODE_PAD);
24
28
  const mode = rich ? chalk.gray(modeLabel) : modeLabel;
@@ -15,6 +15,7 @@ import { resolveNotificationSettings } from "../notifier.js";
15
15
  import { loadUserConfig } from "../../config.js";
16
16
  import { resolveConfiguredMaxFileSizeBytes } from "../fileSize.js";
17
17
  import { formatTokenCount } from "../../oracle/runUtils.js";
18
+ import { formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../../browser/modelDisplay.js";
18
19
  const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
19
20
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
20
21
  const RECENT_WINDOW_HOURS = 24;
@@ -157,7 +158,7 @@ async function showSessionDetail(sessionId) {
157
158
  console.clear();
158
159
  printSessionHeader(meta);
159
160
  if (meta.models && meta.models.length > 0) {
160
- printModelSummaries(meta.models);
161
+ printModelSummaries(meta);
161
162
  }
162
163
  const prompt = await readStoredPrompt(sessionId);
163
164
  if (prompt) {
@@ -250,7 +251,10 @@ function printSessionHeader(meta) {
250
251
  console.log(`${chalk.white("Status:")} ${meta.status}`);
251
252
  console.log(`${chalk.white("Created:")} ${meta.createdAt}`);
252
253
  if (meta.model) {
253
- console.log(`${chalk.white("Model:")} ${meta.model}`);
254
+ const modelLabel = (meta.mode ?? meta.options?.mode) === "browser"
255
+ ? resolveSessionBrowserModelDisplayName(meta)
256
+ : meta.model;
257
+ console.log(`${chalk.white("Model:")} ${modelLabel}`);
254
258
  }
255
259
  const mode = meta.mode ?? meta.options?.mode;
256
260
  if (mode) {
@@ -260,7 +264,8 @@ function printSessionHeader(meta) {
260
264
  console.log(chalk.red(`Error: ${meta.errorMessage}`));
261
265
  }
262
266
  }
263
- function printModelSummaries(models) {
267
+ function printModelSummaries(meta) {
268
+ const models = meta.models ?? [];
264
269
  if (models.length === 0) {
265
270
  return;
266
271
  }
@@ -269,7 +274,10 @@ function printModelSummaries(models) {
269
274
  const usage = run.usage
270
275
  ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
271
276
  : "";
272
- console.log(` - ${chalk.cyan(run.model)} ${run.status}${usage}`);
277
+ const modelLabel = (meta.mode ?? meta.options?.mode) === "browser"
278
+ ? formatSessionBrowserModelWithRequestedKey(meta, run.model)
279
+ : run.model;
280
+ console.log(` - ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
273
281
  }
274
282
  console.log("");
275
283
  }
@@ -21,6 +21,9 @@ export function parseDuration(input, fallback) {
21
21
  let lastIndex = 0;
22
22
  let match = multiDuration.exec(normalized);
23
23
  while (match !== null) {
24
+ if (match.index !== lastIndex) {
25
+ return fallback;
26
+ }
24
27
  total += convertUnit(Number(match[1]), match[2]);
25
28
  lastIndex = multiDuration.lastIndex;
26
29
  match = multiDuration.exec(normalized);
@@ -12,11 +12,29 @@ const GEMINI_UPLOAD_MIME_TYPES = {
12
12
  ".gif": "image/gif",
13
13
  ".jpeg": "image/jpeg",
14
14
  ".jpg": "image/jpeg",
15
+ ".mov": "video/quicktime",
16
+ ".mp4": "video/mp4",
15
17
  ".pdf": "application/pdf",
16
18
  ".png": "image/png",
17
19
  ".svg": "image/svg+xml",
20
+ ".webm": "video/webm",
18
21
  ".webp": "image/webp",
19
22
  };
23
+ /**
24
+ * Resolve the MIME type Gemini should be told an upload carries.
25
+ *
26
+ * Gemini silently discards uploads it cannot type: the run still reports the file as
27
+ * attached, but the model never receives it and answers as though nothing was sent.
28
+ * Anything falling back to `application/octet-stream` is therefore invisible to the model.
29
+ *
30
+ * Only formats confirmed to work against the Gemini web upload endpoint are listed. The
31
+ * endpoint also gates on the file extension, not just the declared type — an `.m4v` byte
32
+ * for byte identical to a working `.mp4`, and declared `video/mp4`, is still dropped — so
33
+ * entries here cannot be extrapolated from what the Gemini API documents.
34
+ */
35
+ export function resolveGeminiUploadMimeType(filePath) {
36
+ return (GEMINI_UPLOAD_MIME_TYPES[path.extname(filePath).toLowerCase()] ?? "application/octet-stream");
37
+ }
20
38
  function getNestedValue(value, pathParts, fallback) {
21
39
  let current = value;
22
40
  for (const part of pathParts) {
@@ -125,7 +143,7 @@ async function uploadGeminiFile(filePath, signal) {
125
143
  const absPath = path.resolve(process.cwd(), filePath);
126
144
  const data = await readFile(absPath);
127
145
  const fileName = path.basename(absPath);
128
- const mimeType = GEMINI_UPLOAD_MIME_TYPES[path.extname(absPath).toLowerCase()] ?? "application/octet-stream";
146
+ const mimeType = resolveGeminiUploadMimeType(absPath);
129
147
  const form = new FormData();
130
148
  form.append("file", new Blob([data], { type: mimeType }), fileName);
131
149
  const res = await fetch(GEMINI_UPLOAD_URL, {
@@ -192,7 +192,14 @@ async function resolveBaseModelConfig(model, options = {}) {
192
192
  export function isProModel(model) {
193
193
  return isKnownModel(model) && PRO_MODELS.has(model);
194
194
  }
195
- const VALID_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
195
+ const VALID_REASONING_EFFORTS = [
196
+ "none",
197
+ "low",
198
+ "medium",
199
+ "high",
200
+ "xhigh",
201
+ "max",
202
+ ];
196
203
  function isRecord(value) {
197
204
  return typeof value === "object" && value !== null && !Array.isArray(value);
198
205
  }
@@ -11,8 +11,15 @@ export function buildPrompt(basePrompt, files, cwd = process.cwd()) {
11
11
  const sectionText = formatFileSections(sections, { includeFileIndex: true });
12
12
  return `${basePrompt.trim()}\n\n${sectionText}`;
13
13
  }
14
- export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, previousResponseId, }) {
14
+ export function buildRequestBody({ modelConfig, reasoningEffort, reasoningMode, systemPrompt, userPrompt, searchEnabled, maxOutputTokens, background, storeResponse, previousResponseId, }) {
15
15
  const searchToolType = modelConfig.searchToolType ?? "web_search_preview";
16
+ const reasoning = modelConfig.reasoning || reasoningEffort || reasoningMode
17
+ ? {
18
+ ...(modelConfig.reasoning ?? {}),
19
+ ...(reasoningEffort ? { effort: reasoningEffort } : {}),
20
+ ...(reasoningMode ? { mode: reasoningMode } : {}),
21
+ }
22
+ : undefined;
16
23
  return {
17
24
  model: modelConfig.apiModel ?? modelConfig.model,
18
25
  previous_response_id: previousResponseId ? previousResponseId : undefined,
@@ -29,7 +36,7 @@ export function buildRequestBody({ modelConfig, systemPrompt, userPrompt, search
29
36
  },
30
37
  ],
31
38
  tools: searchEnabled ? [{ type: searchToolType }] : undefined,
32
- reasoning: modelConfig.reasoning || undefined,
39
+ reasoning,
33
40
  max_output_tokens: maxOutputTokens,
34
41
  background: background ? true : undefined,
35
42
  store: storeResponse ? true : undefined,
@@ -33,6 +33,9 @@ const dim = (text) => (isStdoutTty ? kleur.dim(text) : text);
33
33
  // Default timeout for non-pro API runs (fast models) — give them up to 120s.
34
34
  const DEFAULT_TIMEOUT_NON_PRO_MS = 120_000;
35
35
  const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
36
+ const GPT_5_6_API_MODELS = new Set(["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
37
+ const REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
38
+ const REASONING_MODES = new Set(["standard", "pro"]);
36
39
  const defaultWait = (ms) => new Promise((resolve) => {
37
40
  setTimeout(resolve, ms);
38
41
  });
@@ -69,6 +72,33 @@ function runtimeKeySource({ route, providerMode, optionsApiKey, }) {
69
72
  return "XAI_API_KEY";
70
73
  return optionsApiKey ? "apiKey option" : route.keySource;
71
74
  }
75
+ function validateReasoningOptions(options, route) {
76
+ const { reasoningEffort, reasoningMode } = options;
77
+ if (!reasoningEffort && !reasoningMode)
78
+ return;
79
+ if (reasoningEffort && !REASONING_EFFORTS.has(reasoningEffort)) {
80
+ throw new PromptValidationError(`Invalid reasoning effort "${reasoningEffort}". Expected none, low, medium, high, xhigh, or max.`, { model: options.model, reasoningEffort });
81
+ }
82
+ if (reasoningMode && !REASONING_MODES.has(reasoningMode)) {
83
+ throw new PromptValidationError(`Invalid reasoning mode "${reasoningMode}". Expected standard or pro.`, { model: options.model, reasoningMode });
84
+ }
85
+ if (!GPT_5_6_API_MODELS.has(options.model)) {
86
+ const option = reasoningMode
87
+ ? `Reasoning mode "${reasoningMode}"`
88
+ : `Reasoning effort "${reasoningEffort}"`;
89
+ const guidance = reasoningMode
90
+ ? `Use --model gpt-5.6-sol --reasoning-mode ${reasoningMode}.`
91
+ : `Use --model gpt-5.6-sol --reasoning-effort ${reasoningEffort}.`;
92
+ throw new PromptValidationError(`${option} is available only for GPT-5.6 API models. ${guidance}`, { model: options.model, reasoningEffort, reasoningMode });
93
+ }
94
+ if (reasoningMode &&
95
+ !route.isAzureOpenAI &&
96
+ (route.openRouterFallback ||
97
+ isOpenRouterBaseUrl(route.baseUrl) ||
98
+ isCustomBaseUrl(route.baseUrl))) {
99
+ throw new PromptValidationError("--reasoning-mode requires the OpenAI or Azure OpenAI Responses API; OpenRouter and custom --base-url routes use the Chat Completions adapter.", { model: options.model, reasoningMode });
100
+ }
101
+ }
72
102
  export async function runOracle(options, deps = {}) {
73
103
  const { apiKey: optionsApiKey = options.apiKey, cwd = process.cwd(), fs: fsModule = createFsAdapter(fs), log = console.log, write: sinkWrite = (_text) => true, allowStdout = true, stdoutWrite: stdoutWriteDep, now = () => performance.now(), clientFactory = createDefaultClientFactory(), client, wait = defaultWait, } = deps;
74
104
  const stdoutWrite = allowStdout
@@ -100,6 +130,7 @@ export async function runOracle(options, deps = {}) {
100
130
  const { isAzureOpenAI, azureDeploymentName } = route;
101
131
  const baseUrl = route.baseUrl;
102
132
  const openRouterFallback = route.openRouterFallback;
133
+ validateReasoningOptions(options, route);
103
134
  const logVerbose = (message) => {
104
135
  if (options.verbose) {
105
136
  log(dim(`[verbose] ${message}`));
@@ -125,7 +156,7 @@ export async function runOracle(options, deps = {}) {
125
156
  const minPromptLength = Number.parseInt(process.env.ORACLE_MIN_PROMPT_CHARS ?? "10", 10);
126
157
  const promptLength = options.prompt?.trim().length ?? 0;
127
158
  // Enforce the short-prompt guardrail on pro-tier models because they're costly; cheaper models can run short prompts without blocking.
128
- const isProTierModel = isProModel(options.model);
159
+ const isProTierModel = isProModel(options.model) || options.reasoningMode === "pro";
129
160
  if (isProTierModel && !Number.isNaN(minPromptLength) && promptLength < minPromptLength) {
130
161
  throw new PromptValidationError(`Prompt is too short (<${minPromptLength} chars). This was likely accidental; please provide more detail.`, { minPromptLength, promptLength });
131
162
  }
@@ -205,6 +236,8 @@ export async function runOracle(options, deps = {}) {
205
236
  }
206
237
  const requestBody = buildRequestBody({
207
238
  modelConfig,
239
+ reasoningEffort: options.reasoningEffort,
240
+ reasoningMode: options.reasoningMode,
208
241
  systemPrompt,
209
242
  userPrompt: promptWithFiles,
210
243
  searchEnabled,
@@ -249,6 +282,12 @@ export async function runOracle(options, deps = {}) {
249
282
  if (options.background && !supportsBackground) {
250
283
  log(dim("Background runs are not supported for this model; streaming in foreground instead."));
251
284
  }
285
+ if (options.reasoningMode) {
286
+ log(dim(`Reasoning mode: ${options.reasoningMode}`));
287
+ }
288
+ if (options.reasoningEffort) {
289
+ log(dim(`Reasoning effort: ${options.reasoningEffort}`));
290
+ }
252
291
  if (!options.suppressTips) {
253
292
  if (pendingNoFilesTip) {
254
293
  log(dim(pendingNoFilesTip));
@@ -547,8 +586,9 @@ export async function runOracle(options, deps = {}) {
547
586
  },
548
587
  })?.totalUsd
549
588
  : undefined;
550
- const effortLabel = modelConfig.reasoning?.effort;
551
- const modelLabel = effortLabel ? `${modelConfig.model}[${effortLabel}]` : modelConfig.model;
589
+ const effortLabel = options.reasoningEffort ?? modelConfig.reasoning?.effort;
590
+ const reasoningLabel = [options.reasoningMode, effortLabel].filter(Boolean).join("/");
591
+ const modelLabel = reasoningLabel ? `${modelConfig.model}[${reasoningLabel}]` : modelConfig.model;
552
592
  const sessionIdContainsModel = typeof options.sessionId === "string" &&
553
593
  options.sessionId.toLowerCase().includes(modelConfig.model.toLowerCase());
554
594
  const tokensDisplay = [inputTokens, outputTokens, reasoningTokens, totalTokens]