@steipete/oracle 0.19.0 → 0.20.1

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 (41) hide show
  1. package/dist/bin/oracle-cli.js +72 -29
  2. package/dist/src/browser/actions/deepResearch.js +168 -44
  3. package/dist/src/browser/actions/modelSelection.js +78 -1
  4. package/dist/src/browser/actions/promptComposer.js +3 -0
  5. package/dist/src/browser/actions/thinkingTime.js +49 -6
  6. package/dist/src/browser/actions/webSearch.js +96 -0
  7. package/dist/src/browser/chromeLifecycle.js +28 -16
  8. package/dist/src/browser/config.js +1 -1
  9. package/dist/src/browser/index.js +144 -49
  10. package/dist/src/browser/liveTabs.js +11 -4
  11. package/dist/src/browser/profileState.js +6 -1
  12. package/dist/src/browser/promptFingerprint.js +54 -0
  13. package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
  14. package/dist/src/browser/reattach.js +178 -109
  15. package/dist/src/browser/recoveryTarget.js +156 -0
  16. package/dist/src/browser/sessionRunner.js +33 -8
  17. package/dist/src/browser/tabLeaseRegistry.js +20 -0
  18. package/dist/src/browser/targetClaim.js +54 -0
  19. package/dist/src/cli/browserConfig.js +33 -3
  20. package/dist/src/cli/browserDefaults.js +3 -0
  21. package/dist/src/cli/browserTabs.js +38 -3
  22. package/dist/src/cli/detach.js +10 -1
  23. package/dist/src/cli/detachedSession.js +36 -0
  24. package/dist/src/cli/options.js +15 -0
  25. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  26. package/dist/src/cli/runOptions.js +19 -4
  27. package/dist/src/cli/sessionDisplay.js +15 -8
  28. package/dist/src/cli/sessionRunner.js +153 -41
  29. package/dist/src/mcp/tools/consult.js +2 -2
  30. package/dist/src/mcp/types.js +1 -1
  31. package/dist/src/oracle/config.js +14 -0
  32. package/dist/src/oracle/geminiModels.js +1 -0
  33. package/dist/src/oracle/run.js +27 -4
  34. package/dist/src/remote/client.js +3 -0
  35. package/dist/src/remote/server.js +2 -0
  36. package/dist/src/sessionManager.js +8 -1
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  38. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  39. package/package.json +10 -10
  40. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  41. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -9,7 +9,6 @@ import { formatResponseMetadata, formatTransportMetadata } from "./sessionDispla
9
9
  import { markErrorLogged } from "./errorUtils.js";
10
10
  import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from "./notifier.js";
11
11
  import { sessionStore } from "../sessionStore.js";
12
- import { wait } from "../sessionManager.js";
13
12
  import { runMultiModelApiSession } from "../oracle/multiModelRunner.js";
14
13
  import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from "../oracle/config.js";
15
14
  import { isKnownModel } from "../oracle/modelResolver.js";
@@ -22,30 +21,71 @@ import { sanitizeOscProgress } from "./oscUtils.js";
22
21
  import { readFiles } from "../oracle/files.js";
23
22
  import { cwd as getCwd } from "node:process";
24
23
  import { resumeBrowserSession } from "../browser/reattach.js";
24
+ import { recoveryCaptureFromRuntime, retireCancelledBrowserTarget, retireRecoveredBrowserTarget, } from "../browser/recoveryTarget.js";
25
25
  import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
26
- import { estimateTokenCount } from "../browser/utils.js";
26
+ import { delay, estimateTokenCount } from "../browser/utils.js";
27
27
  import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
28
28
  import { formatElapsed } from "../oracle/format.js";
29
29
  import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
30
+ import { BrowserRunCancelledError } from "../oracle/errors.js";
30
31
  const isTty = process.stdout.isTTY;
31
32
  const dim = (text) => (isTty ? kleur.dim(text) : text);
32
- export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
33
+ export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, signal, }) {
33
34
  const writeInline = (chunk) => {
34
35
  // Keep session logs intact while still echoing inline output to the user.
35
36
  write(chunk);
36
37
  return muteStdout ? true : process.stdout.write(chunk);
37
38
  };
38
39
  let currentBrowser = browserConfig
39
- ? { config: browserConfig }
40
+ ? {
41
+ config: browserConfig,
42
+ ...(mode === "browser" && sessionMeta.browser?.runtime?.submittedPromptHash !== undefined
43
+ ? { runtime: { submittedPromptHash: null } }
44
+ : {}),
45
+ }
40
46
  : sessionMeta.browser;
41
47
  await sessionStore.updateSession(sessionMeta.id, {
42
48
  status: "running",
43
49
  startedAt: new Date().toISOString(),
44
50
  mode,
45
- ...(browserConfig ? { browser: { config: browserConfig } } : {}),
51
+ ...(browserConfig ? { browser: currentBrowser } : {}),
46
52
  });
47
53
  const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
48
54
  const modelForStatus = runOptions.model ?? sessionMeta.model;
55
+ const checkBrowserCancellation = () => {
56
+ if (mode === "browser" && signal?.aborted)
57
+ throw new BrowserRunCancelledError();
58
+ };
59
+ const finalizeBrowserCancellation = async (cancellationError) => {
60
+ const completedAt = new Date().toISOString();
61
+ log("Browser run cancelled.");
62
+ if (modelForStatus) {
63
+ await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
64
+ status: "cancelled",
65
+ completedAt,
66
+ response: { status: "cancelled" },
67
+ error: undefined,
68
+ transport: undefined,
69
+ });
70
+ }
71
+ await sessionStore.updateSession(sessionMeta.id, {
72
+ status: "cancelled",
73
+ completedAt,
74
+ errorMessage: undefined,
75
+ mode,
76
+ browser: browserConfig
77
+ ? {
78
+ ...currentBrowser,
79
+ config: browserConfig,
80
+ }
81
+ : undefined,
82
+ response: { status: "cancelled" },
83
+ error: undefined,
84
+ transport: undefined,
85
+ });
86
+ await retireCancelledBrowserTarget(sessionMeta.id, recoveryCaptureFromRuntime(currentBrowser?.runtime), log);
87
+ throw cancellationError;
88
+ };
49
89
  try {
50
90
  if (mode === "browser") {
51
91
  if (!browserConfig) {
@@ -79,13 +119,17 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
79
119
  browserConfig,
80
120
  cwd,
81
121
  log,
122
+ signal,
82
123
  }, runnerDeps);
124
+ checkBrowserCancellation();
83
125
  const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
126
+ checkBrowserCancellation();
84
127
  const outputArtifacts = await copyBrowserOutputArtifacts({
85
128
  outputPath: writtenOutputPath,
86
129
  savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
87
130
  log,
88
131
  });
132
+ checkBrowserCancellation();
89
133
  const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
90
134
  await sendSessionNotification({
91
135
  sessionId: sessionMeta.id,
@@ -95,13 +139,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
95
139
  usage: result.usage,
96
140
  characters: result.answerText?.length,
97
141
  }, notificationSettings, log, result.answerText?.slice(0, 140));
142
+ checkBrowserCancellation();
98
143
  if (modelForStatus) {
99
144
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
100
145
  status: "completed",
101
146
  completedAt: new Date().toISOString(),
102
147
  usage: result.usage,
103
148
  });
149
+ checkBrowserCancellation();
104
150
  }
151
+ checkBrowserCancellation();
105
152
  await sessionStore.updateSession(sessionMeta.id, {
106
153
  status: "completed",
107
154
  completedAt: new Date().toISOString(),
@@ -400,6 +447,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
400
447
  });
401
448
  }
402
449
  catch (error) {
450
+ if (error instanceof BrowserRunCancelledError) {
451
+ return await finalizeBrowserCancellation(error);
452
+ }
403
453
  const message = formatError(error);
404
454
  log(`ERROR: ${message}`);
405
455
  markErrorLogged(error);
@@ -425,6 +475,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
425
475
  const runtime = userError.details
426
476
  ?.runtime;
427
477
  const recoverableRuntime = runtime ?? currentBrowser?.runtime;
478
+ currentBrowser = {
479
+ ...currentBrowser,
480
+ config: browserConfig,
481
+ runtime: recoverableRuntime,
482
+ };
428
483
  if (!hasRecoverableChatGptConversation(recoverableRuntime) &&
429
484
  recoverableRuntime?.promptSubmitted !== true) {
430
485
  log(dim("Chrome disconnected before a ChatGPT conversation was created; marking session error."));
@@ -494,22 +549,32 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
494
549
  const connectionLostIntervalMs = configuredIntervalMs > 0
495
550
  ? configuredIntervalMs
496
551
  : Math.max(1_000, Math.min(browserConfig?.timeoutMs ?? 30_000, 30_000));
497
- const success = await autoReattachUntilComplete({
498
- sessionMeta,
499
- runtime: recoverableRuntime ?? undefined,
500
- browserConfig: {
501
- ...browserConfig,
502
- autoReattachIntervalMs: connectionLostIntervalMs,
503
- autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
504
- autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
505
- },
506
- browserMetadata: currentBrowser,
507
- runOptions,
508
- modelForStatus,
509
- notificationSettings,
510
- log,
511
- maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
512
- });
552
+ let success;
553
+ try {
554
+ success = await autoReattachUntilComplete({
555
+ sessionMeta,
556
+ runtime: recoverableRuntime ?? undefined,
557
+ browserConfig: {
558
+ ...browserConfig,
559
+ autoReattachIntervalMs: connectionLostIntervalMs,
560
+ autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
561
+ autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
562
+ },
563
+ browserMetadata: currentBrowser,
564
+ runOptions,
565
+ modelForStatus,
566
+ notificationSettings,
567
+ log,
568
+ maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
569
+ signal,
570
+ });
571
+ }
572
+ catch (recoveryError) {
573
+ if (signal?.aborted || recoveryError instanceof BrowserRunCancelledError) {
574
+ return await finalizeBrowserCancellation(new BrowserRunCancelledError());
575
+ }
576
+ throw recoveryError;
577
+ }
513
578
  if (success) {
514
579
  return;
515
580
  }
@@ -530,6 +595,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
530
595
  };
531
596
  const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
532
597
  const autoRuntime = runtime ?? currentBrowser?.runtime;
598
+ currentBrowser = {
599
+ ...currentBrowser,
600
+ config: browserConfig,
601
+ runtime: autoRuntime,
602
+ };
533
603
  const willAutoReattach = autoReattachIntervalMs > 0 && Boolean(autoRuntime);
534
604
  if (willAutoReattach) {
535
605
  if (modelForStatus) {
@@ -553,16 +623,26 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
553
623
  response: timeoutResponse,
554
624
  error: timeoutError,
555
625
  });
556
- const success = await autoReattachUntilComplete({
557
- sessionMeta,
558
- runtime: autoRuntime,
559
- browserConfig,
560
- browserMetadata: currentBrowser,
561
- runOptions,
562
- modelForStatus,
563
- notificationSettings,
564
- log,
565
- });
626
+ let success;
627
+ try {
628
+ success = await autoReattachUntilComplete({
629
+ sessionMeta,
630
+ runtime: autoRuntime,
631
+ browserConfig,
632
+ browserMetadata: currentBrowser,
633
+ runOptions,
634
+ modelForStatus,
635
+ notificationSettings,
636
+ log,
637
+ signal,
638
+ });
639
+ }
640
+ catch (recoveryError) {
641
+ if (signal?.aborted || recoveryError instanceof BrowserRunCancelledError) {
642
+ return await finalizeBrowserCancellation(new BrowserRunCancelledError());
643
+ }
644
+ throw recoveryError;
645
+ }
566
646
  if (success) {
567
647
  return;
568
648
  }
@@ -975,7 +1055,7 @@ async function writeAssistantOutput(targetPath, content, log) {
975
1055
  log(dim(`write-output failed (${reason}); session completed anyway.`));
976
1056
  }
977
1057
  }
978
- async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, }) {
1058
+ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, signal, }) {
979
1059
  if (!runtime || !browserConfig) {
980
1060
  log(dim("Auto-reattach disabled: missing runtime or browser config."));
981
1061
  return false;
@@ -990,12 +1070,16 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
990
1070
  120_000;
991
1071
  const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
992
1072
  const maxDeadline = Date.now() + maxTotalMs;
1073
+ const checkCancellation = () => {
1074
+ if (signal?.aborted)
1075
+ throw new BrowserRunCancelledError();
1076
+ };
993
1077
  const attemptLimit = typeof maxAttempts === "number" && maxAttempts > 0
994
1078
  ? Math.floor(maxAttempts)
995
1079
  : Number.POSITIVE_INFINITY;
996
1080
  if (delayMs > 0) {
997
1081
  log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
998
- await wait(delayMs);
1082
+ await delay(delayMs, signal);
999
1083
  }
1000
1084
  if (Number.isFinite(attemptLimit)) {
1001
1085
  log(dim(`Auto-reattach will try up to ${attemptLimit} attempt(s).`));
@@ -1011,6 +1095,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1011
1095
  logger.verbose = true;
1012
1096
  let attempt = 0;
1013
1097
  for (;;) {
1098
+ checkCancellation();
1014
1099
  const remainingBudgetMs = maxDeadline - Date.now();
1015
1100
  if (remainingBudgetMs <= 0) {
1016
1101
  log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
@@ -1024,9 +1109,11 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1024
1109
  ...browserConfig,
1025
1110
  timeoutMs,
1026
1111
  };
1027
- const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
1112
+ const result = await awaitBrowserCancellation(resumeBrowserSession(runtime, reattachConfig, logger, {
1028
1113
  promptPreview: sessionMeta.promptPreview,
1029
- });
1114
+ signal,
1115
+ }), signal);
1116
+ checkCancellation();
1030
1117
  captureSucceeded = true;
1031
1118
  const answerText = result.answerMarkdown || result.answerText || "";
1032
1119
  const outputTokens = estimateTokenCount(answerText);
@@ -1039,11 +1126,11 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1039
1126
  existingArtifacts: sessionMeta.artifacts,
1040
1127
  logger,
1041
1128
  });
1042
- const logWriter = sessionStore.createLogWriter(sessionMeta.id);
1043
- logWriter.logLine(`[auto-reattach] captured assistant response on attempt ${attempt}`);
1044
- logWriter.logLine("Answer:");
1045
- logWriter.logLine(answerText);
1046
- logWriter.stream.end();
1129
+ checkCancellation();
1130
+ const paths = await sessionStore.getPaths(sessionMeta.id);
1131
+ checkCancellation();
1132
+ await fs.appendFile(paths.log, `[auto-reattach] captured assistant response on attempt ${attempt}\nAnswer:\n${answerText}\n`, "utf8");
1133
+ checkCancellation();
1047
1134
  if (modelForStatus) {
1048
1135
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
1049
1136
  status: "completed",
@@ -1055,8 +1142,10 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1055
1142
  totalTokens: outputTokens,
1056
1143
  },
1057
1144
  });
1145
+ checkCancellation();
1058
1146
  }
1059
1147
  await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
1148
+ checkCancellation();
1060
1149
  await sendSessionNotification({
1061
1150
  sessionId: sessionMeta.id,
1062
1151
  sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
@@ -1068,6 +1157,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1068
1157
  },
1069
1158
  characters: answerText.length,
1070
1159
  }, notificationSettings, log, answerText.slice(0, 140));
1160
+ checkCancellation();
1071
1161
  await sessionStore.updateSession(sessionMeta.id, {
1072
1162
  status: "completed",
1073
1163
  completedAt: new Date().toISOString(),
@@ -1089,9 +1179,13 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1089
1179
  transport: undefined,
1090
1180
  });
1091
1181
  log(kleur.green("Auto-reattach succeeded; session marked completed."));
1182
+ await retireRecoveredBrowserTarget(sessionMeta.id, result.captureTarget, logger);
1092
1183
  return true;
1093
1184
  }
1094
1185
  catch (error) {
1186
+ if (signal?.aborted || error instanceof BrowserRunCancelledError) {
1187
+ throw new BrowserRunCancelledError();
1188
+ }
1095
1189
  if (captureSucceeded) {
1096
1190
  const message = formatError(error);
1097
1191
  if (modelForStatus) {
@@ -1129,7 +1223,25 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1129
1223
  log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
1130
1224
  return false;
1131
1225
  }
1132
- await wait(Math.min(intervalMs, remainingAfterAttemptMs));
1226
+ await delay(Math.min(intervalMs, remainingAfterAttemptMs), signal);
1227
+ }
1228
+ }
1229
+ async function awaitBrowserCancellation(task, signal) {
1230
+ if (!signal)
1231
+ return task;
1232
+ if (signal.aborted)
1233
+ throw new BrowserRunCancelledError();
1234
+ let abort;
1235
+ const cancelled = new Promise((_, reject) => {
1236
+ abort = () => reject(new BrowserRunCancelledError());
1237
+ signal.addEventListener("abort", abort, { once: true });
1238
+ });
1239
+ try {
1240
+ return await Promise.race([task, cancelled]);
1241
+ }
1242
+ finally {
1243
+ if (abort)
1244
+ signal.removeEventListener("abort", abort);
1133
1245
  }
1134
1246
  }
1135
1247
  export function deriveModelOutputPath(basePath, model) {
@@ -74,9 +74,9 @@ const consultInputShape = {
74
74
  .optional()
75
75
  .describe("Browser-only: model picker strategy. Mirrors the CLI --browser-model-strategy flag."),
76
76
  browserResearchMode: z
77
- .enum(["deep"])
77
+ .enum(["search", "deep"])
78
78
  .optional()
79
- .describe("Browser-only: activate ChatGPT Deep Research mode for broad web research."),
79
+ .describe("Browser-only: explicitly select ChatGPT Web Search or Deep Research."),
80
80
  browserArchive: z
81
81
  .enum(["auto", "always", "never"])
82
82
  .optional()
@@ -23,7 +23,7 @@ export const consultInputSchema = z
23
23
  browserBundleFormat: z.enum(["auto", "text", "zip"]).optional(),
24
24
  browserThinkingTime: browserThinkingTimeInputSchema.optional(),
25
25
  browserModelStrategy: z.enum(["select", "current", "ignore"]).optional(),
26
- browserResearchMode: z.enum(["deep"]).optional(),
26
+ browserResearchMode: z.enum(["search", "deep"]).optional(),
27
27
  browserArchive: z.enum(["auto", "always", "never"]).optional(),
28
28
  browserFollowUps: z.array(z.string()).optional(),
29
29
  browserKeepBrowser: z.boolean().optional(),
@@ -30,6 +30,20 @@ const countTokensAnthropic = (input) => {
30
30
  // supported limit at the base-rate boundary until cost estimation supports tiers.
31
31
  const GPT_5_6_BASE_RATE_INPUT_LIMIT = 272_000;
32
32
  export const MODEL_CONFIGS = {
33
+ "gpt-6-astra": {
34
+ model: "gpt-6-astra",
35
+ provider: "openai",
36
+ tokenizer: countTokensGpt5,
37
+ // Conservative base-rate budget, not Astra's maximum context window.
38
+ // https://developers.openai.com/api/docs/models/gpt-6-astra
39
+ inputLimit: 272_000,
40
+ pricing: {
41
+ inputPerToken: 10 / 1_000_000,
42
+ outputPerToken: 50 / 1_000_000,
43
+ },
44
+ reasoning: { effort: "xhigh" },
45
+ searchToolType: "web_search",
46
+ },
33
47
  "gpt-5.6": {
34
48
  model: "gpt-5.6",
35
49
  provider: "openai",
@@ -1,4 +1,5 @@
1
1
  const MODEL_ID_MAP = {
2
+ "gpt-6-astra": "gpt-6-astra",
2
3
  "gemini-3.1-flash-lite": "gemini-3.1-flash-lite",
3
4
  "gemini-3.1-pro": "gemini-3.1-pro-preview",
4
5
  "gemini-3.5-flash": "gemini-3.5-flash",
@@ -36,6 +36,15 @@ const DEFAULT_TIMEOUT_PRO_MS = 60 * 60 * 1000;
36
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
37
  const REASONING_EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
38
38
  const REASONING_MODES = new Set(["standard", "pro"]);
39
+ // Astra has a narrower effort range than the GPT-5.6 API family. Keep this
40
+ // capability separate from GPT_5_6_API_MODELS so adding a model cannot
41
+ // accidentally inherit a family's exact option set.
42
+ const MODEL_REASONING_CAPABILITIES = new Map([
43
+ [
44
+ "gpt-6-astra",
45
+ { efforts: new Set(["low", "medium", "high", "xhigh", "max"]), modes: REASONING_MODES },
46
+ ],
47
+ ]);
39
48
  const defaultWait = (ms) => new Promise((resolve) => {
40
49
  setTimeout(resolve, ms);
41
50
  });
@@ -72,9 +81,13 @@ function runtimeKeySource({ route, providerMode, optionsApiKey, }) {
72
81
  return "XAI_API_KEY";
73
82
  return optionsApiKey ? "apiKey option" : route.keySource;
74
83
  }
75
- function validateReasoningOptions(options, route) {
84
+ function validateReasoningOptions(options, route, modelConfig) {
76
85
  const { reasoningEffort, reasoningMode } = options;
77
- if (!reasoningEffort && !reasoningMode)
86
+ const capabilities = GPT_5_6_API_MODELS.has(options.model)
87
+ ? { efforts: REASONING_EFFORTS, modes: REASONING_MODES }
88
+ : MODEL_REASONING_CAPABILITIES.get(options.model);
89
+ const effectiveEffort = reasoningEffort ?? modelConfig?.reasoning?.effort;
90
+ if (!reasoningEffort && !reasoningMode && !effectiveEffort)
78
91
  return;
79
92
  if (reasoningEffort && !REASONING_EFFORTS.has(reasoningEffort)) {
80
93
  throw new PromptValidationError(`Invalid reasoning effort "${reasoningEffort}". Expected none, low, medium, high, xhigh, or max.`, { model: options.model, reasoningEffort });
@@ -82,14 +95,20 @@ function validateReasoningOptions(options, route) {
82
95
  if (reasoningMode && !REASONING_MODES.has(reasoningMode)) {
83
96
  throw new PromptValidationError(`Invalid reasoning mode "${reasoningMode}". Expected standard or pro.`, { model: options.model, reasoningMode });
84
97
  }
85
- if (!GPT_5_6_API_MODELS.has(options.model)) {
98
+ if ((reasoningEffort || reasoningMode) && !capabilities) {
86
99
  const option = reasoningMode
87
100
  ? `Reasoning mode "${reasoningMode}"`
88
101
  : `Reasoning effort "${reasoningEffort}"`;
89
102
  const guidance = reasoningMode
90
103
  ? `Use --model gpt-5.6-sol --reasoning-mode ${reasoningMode}.`
91
104
  : `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 });
105
+ throw new PromptValidationError(`${option} is available only for GPT-5.6 or GPT-6 Astra API models. ${guidance}`, { model: options.model, reasoningEffort, reasoningMode });
106
+ }
107
+ if (effectiveEffort && capabilities && !capabilities.efforts.has(effectiveEffort)) {
108
+ throw new PromptValidationError(`Reasoning effort "${effectiveEffort}" is not supported for ${options.model}. Expected low, medium, high, xhigh, or max.`, { model: options.model, reasoningEffort: effectiveEffort, reasoningMode });
109
+ }
110
+ if (reasoningMode && capabilities && !capabilities.modes.has(reasoningMode)) {
111
+ throw new PromptValidationError(`Reasoning mode "${reasoningMode}" is not supported for ${options.model}. Expected standard or pro.`, { model: options.model, reasoningEffort, reasoningMode });
93
112
  }
94
113
  if (reasoningMode &&
95
114
  !route.isAzureOpenAI &&
@@ -166,6 +185,10 @@ export async function runOracle(options, deps = {}) {
166
185
  openRouterApiKey: resolverOpenRouterApiKey,
167
186
  modelOverrides: options.modelOverrides,
168
187
  });
188
+ // Validate the resolved default as well as explicit flags. This catches a
189
+ // model override such as Astra + reasoning.effort=none, while allowing an
190
+ // explicit supported effort to replace an invalid bundled/overridden default.
191
+ validateReasoningOptions(options, route, modelConfig);
169
192
  const isLongRunningModel = isProTierModel;
170
193
  const supportsBackground = modelConfig.supportsBackground !== false;
171
194
  const useBackground = supportsBackground ? (options.background ?? isLongRunningModel) : false;
@@ -13,6 +13,9 @@ import { BrowserRunCancelledError } from "../oracle/errors.js";
13
13
  export function createRemoteBrowserExecutor({ host, token }) {
14
14
  // Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
15
15
  return async function remoteBrowserExecutor(options) {
16
+ if (options.config?.researchMode === "search") {
17
+ throw new Error("Web Search is a local browser pilot; --remote-host does not negotiate this capability yet. Use local Chrome or --browser-attach-running.");
18
+ }
16
19
  const callerSignal = options.signal;
17
20
  if (callerSignal?.aborted)
18
21
  throw new BrowserRunCancelledError("Browser run cancelled before the request was sent.");
@@ -814,10 +814,12 @@ function sanitizeResult(result, warnings = []) {
814
814
  answerChars: result.answerChars,
815
815
  modelSelection: result.modelSelection,
816
816
  thinkingSelection: result.thinkingSelection,
817
+ researchPlan: result.researchPlan,
817
818
  archive: result.archive,
818
819
  tabUrl: result.tabUrl,
819
820
  conversationId: result.conversationId,
820
821
  promptSubmitted: result.promptSubmitted,
822
+ submittedPromptHash: result.submittedPromptHash,
821
823
  warnings: warnings.length > 0 ? warnings : undefined,
822
824
  chromePid: undefined,
823
825
  chromePort: undefined,
@@ -304,7 +304,14 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
304
304
  })),
305
305
  cwd,
306
306
  mode,
307
- browser: browserConfig ? { config: browserConfig } : undefined,
307
+ browser: mode === "browser"
308
+ ? {
309
+ ...(browserConfig ? { config: browserConfig } : {}),
310
+ runtime: { submittedPromptHash: null },
311
+ }
312
+ : browserConfig
313
+ ? { config: browserConfig }
314
+ : undefined,
308
315
  notifications,
309
316
  options: {
310
317
  prompt: options.prompt,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "CLI wrapper around OpenAI Responses API with GPT-5.6 Sol, GPT-5.6, GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
5
5
  "keywords": [],
6
6
  "homepage": "https://askoracle.sh",
@@ -61,7 +61,7 @@
61
61
  "@google/genai": "^2.21.0",
62
62
  "@google/generative-ai": "^0.24.1",
63
63
  "@modelcontextprotocol/server": "^2.0.0",
64
- "@steipete/sweet-cookie": "^0.4.2",
64
+ "@steipete/sweet-cookie": "^0.4.3",
65
65
  "chalk": "^6.0.0",
66
66
  "chrome-launcher": "^1.2.1",
67
67
  "chrome-remote-interface": "^0.34.0",
@@ -70,17 +70,17 @@
70
70
  "dotenv": "^17.4.2",
71
71
  "fast-glob": "^3.3.3",
72
72
  "gpt-tokenizer": "^4.0.0",
73
- "inquirer": "14.2.1",
73
+ "inquirer": "14.2.2",
74
74
  "json5": "^2.2.3",
75
75
  "kleur": "^4.1.5",
76
76
  "markdansi": "0.3.3",
77
- "openai": "^7.10.0",
77
+ "openai": "^7.13.0",
78
78
  "osc-progress": "^0.3.3",
79
79
  "qs": "^6.16.0",
80
80
  "shiki": "^4.4.3",
81
81
  "toasted-notifier": "^10.1.0",
82
- "tokentally": "^0.1.4",
83
- "zod": "^4.5.4"
82
+ "tokentally": "^0.1.6",
83
+ "zod": "^4.6.0"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@anthropic-ai/tokenizer": "^0.0.4",
@@ -88,13 +88,13 @@
88
88
  "@modelcontextprotocol/sdk": "^1.30.0",
89
89
  "@types/chrome-remote-interface": "^0.34.0",
90
90
  "@types/inquirer": "^9.0.10",
91
- "@types/node": "^26.4.1",
91
+ "@types/node": "^26.5.1",
92
92
  "@vitest/coverage-v8": "5.0.0",
93
- "devtools-protocol": "0.0.1692173",
93
+ "devtools-protocol": "0.0.1694333",
94
94
  "es-toolkit": "^1.52.0",
95
95
  "esbuild": "^0.28.2",
96
- "oxfmt": "0.66.0",
97
- "oxlint": "^1.81.0",
96
+ "oxfmt": "0.67.0",
97
+ "oxlint": "^1.82.0",
98
98
  "puppeteer-core": "^25.10.0",
99
99
  "tsx": "^4.23.13",
100
100
  "typescript": "^7.0.2",