@steipete/oracle 0.15.2 → 0.16.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 (79) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +1 -1
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/assistantResponse.js +218 -126
  5. package/dist/src/browser/actions/modelSelection.js +133 -22
  6. package/dist/src/browser/actions/navigation.js +235 -14
  7. package/dist/src/browser/actions/thinkingStatus.js +228 -0
  8. package/dist/src/browser/actions/thinkingTime.js +68 -11
  9. package/dist/src/browser/cdpLiveness.js +78 -0
  10. package/dist/src/browser/chromeLifecycle.js +112 -30
  11. package/dist/src/browser/config.js +14 -1
  12. package/dist/src/browser/constants.js +5 -1
  13. package/dist/src/browser/controlPlan.js +2 -2
  14. package/dist/src/browser/conversationUrl.js +16 -0
  15. package/dist/src/browser/conversationUrlMonitor.js +2 -4
  16. package/dist/src/browser/cookies.js +6 -4
  17. package/dist/src/browser/index.js +213 -57
  18. package/dist/src/browser/liveTabs.js +115 -33
  19. package/dist/src/browser/pageActions.js +1 -1
  20. package/dist/src/browser/projectSourcesRunner.js +4 -4
  21. package/dist/src/browser/reattach.js +2 -2
  22. package/dist/src/browser/reattachHelpers.js +3 -5
  23. package/dist/src/browser/reattachability.js +2 -1
  24. package/dist/src/browser/recoverConversation.js +90 -29
  25. package/dist/src/browser/tabLeaseRegistry.js +9 -3
  26. package/dist/src/cli/browserConfig.js +5 -1
  27. package/dist/src/cli/browserDefaults.js +4 -1
  28. package/dist/src/cli/browserTabs.js +54 -33
  29. package/dist/src/cli/options.js +24 -0
  30. package/dist/src/cli/runOptions.js +6 -1
  31. package/dist/src/cli/sessionDisplay.js +38 -14
  32. package/dist/src/cli/sessionRunner.js +49 -2
  33. package/dist/src/oracle/config.js +25 -0
  34. package/dist/src/oracle/geminiModels.js +2 -0
  35. package/dist/src/remote/server.js +8 -0
  36. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  38. package/package.json +13 -14
  39. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  40. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  41. package/dist/bin/oracle.js +0 -569
  42. package/dist/docs-site/.nojekyll +0 -0
  43. package/dist/docs-site/CNAME +0 -1
  44. package/dist/docs-site/RELEASING.html +0 -410
  45. package/dist/docs-site/agents.html +0 -374
  46. package/dist/docs-site/anthropic.html +0 -368
  47. package/dist/docs-site/bridge.html +0 -416
  48. package/dist/docs-site/browser-mode.html +0 -594
  49. package/dist/docs-site/chromium-forks.html +0 -347
  50. package/dist/docs-site/cli-reference.html +0 -346
  51. package/dist/docs-site/configuration.html +0 -462
  52. package/dist/docs-site/favicon.svg +0 -14
  53. package/dist/docs-site/followup.html +0 -375
  54. package/dist/docs-site/gemini.html +0 -383
  55. package/dist/docs-site/grok.html +0 -325
  56. package/dist/docs-site/index.html +0 -360
  57. package/dist/docs-site/install.html +0 -335
  58. package/dist/docs-site/linux.html +0 -321
  59. package/dist/docs-site/llms.txt +0 -43
  60. package/dist/docs-site/manual-tests.html +0 -596
  61. package/dist/docs-site/mcp.html +0 -391
  62. package/dist/docs-site/multimodel.html +0 -364
  63. package/dist/docs-site/mythical-pro-agents.html +0 -360
  64. package/dist/docs-site/notifier.html +0 -338
  65. package/dist/docs-site/openai-endpoints.html +0 -399
  66. package/dist/docs-site/openrouter.html +0 -344
  67. package/dist/docs-site/quickstart.html +0 -369
  68. package/dist/docs-site/refactor/ux.html +0 -532
  69. package/dist/docs-site/sessions.html +0 -388
  70. package/dist/docs-site/social-card.png +0 -0
  71. package/dist/docs-site/social-card.svg +0 -79
  72. package/dist/docs-site/spec.html +0 -363
  73. package/dist/docs-site/testing.html +0 -320
  74. package/dist/docs-site/tui-debug.html +0 -326
  75. package/dist/docs-site/windows-work.html +0 -323
  76. package/dist/docs-site/windows.html +0 -320
  77. package/dist/src/browser/chromeCookies.js +0 -312
  78. package/dist/src/browser/keytarShim.js +0 -56
  79. package/dist/src/browser/windowsCookies.js +0 -219
@@ -3,10 +3,32 @@ import { createHash } from "node:crypto";
3
3
  import chalk from "chalk";
4
4
  import { sessionStore } from "../sessionStore.js";
5
5
  import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, extractConversationIdFromUrl, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
6
- import { recoverConversationTab } from "../browser/recoverConversation.js";
6
+ import { isRecoveredConversationHarvestReady, recoverConversationTab, } from "../browser/recoverConversation.js";
7
7
  import { resolveOutputPath } from "./writeOutputPath.js";
8
8
  const LIVE_POLL_MS = 2000;
9
9
  const DEFAULT_STALL_THRESHOLD_MS = 60_000;
10
+ function isRecoverableMissingTabError(message) {
11
+ return (message.includes("No ChatGPT tab matched") ||
12
+ message.includes("No live ChatGPT tabs found") ||
13
+ message.includes("ECONNREFUSED") ||
14
+ message.includes("Could not connect"));
15
+ }
16
+ function finishRecoveredChrome(recoveredChrome, closeAfterRecover) {
17
+ if (!recoveredChrome) {
18
+ return;
19
+ }
20
+ try {
21
+ if (closeAfterRecover) {
22
+ recoveredChrome.kill();
23
+ }
24
+ else {
25
+ recoveredChrome.process?.unref?.();
26
+ }
27
+ }
28
+ catch {
29
+ // best-effort cleanup
30
+ }
31
+ }
10
32
  function sessionBrowserEndpoint(meta) {
11
33
  const runtime = meta?.browser?.runtime ?? {};
12
34
  const remote = meta?.browser?.config?.remoteChrome ?? {};
@@ -150,12 +172,13 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
150
172
  if (!meta) {
151
173
  throw new Error(`No session found with ID ${sessionId}.`);
152
174
  }
153
- const initialEndpoint = sessionBrowserEndpoint(meta) ?? {
175
+ const recordedEndpoint = sessionBrowserEndpoint(meta);
176
+ const initialEndpoint = recordedEndpoint ?? {
154
177
  host: DEFAULT_REMOTE_CHROME_HOST,
155
178
  port: DEFAULT_REMOTE_CHROME_PORT,
156
179
  };
157
180
  const ref = options.browserTabRef ?? resolveSessionTabRef(meta);
158
- const recoverIfMissing = options.recoverIfMissing !== false;
181
+ const recoverIfMissing = options.recoverIfMissing !== false && !options.browserTabRef;
159
182
  let recoveredChrome = null;
160
183
  try {
161
184
  let harvested;
@@ -169,19 +192,18 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
169
192
  }
170
193
  catch (error) {
171
194
  const message = error instanceof Error ? error.message : String(error);
172
- const isMissingTabError = message.includes("No ChatGPT tab matched") ||
173
- message.includes("ECONNREFUSED") ||
174
- message.includes("Could not connect");
175
- if (!isMissingTabError || !recoverIfMissing) {
195
+ if (!isRecoverableMissingTabError(message) || !recoverIfMissing) {
176
196
  throw error;
177
197
  }
178
198
  console.log(chalk.yellow(`No live ChatGPT tab matched session "${sessionId}". Attempting recovery by reopening the saved conversation URL.`));
179
- const recovered = await recoverConversationTab(meta, (line) => console.log(line));
199
+ const recovered = await recoverConversationTab(meta, (line) => console.log(line), {
200
+ existingEndpoint: recordedEndpoint ?? undefined,
201
+ });
180
202
  recoveredChrome = recovered.chrome;
181
203
  harvested = await harvestChatGptTab({
182
204
  host: recovered.host,
183
205
  port: recovered.port,
184
- ref: recovered.url,
206
+ ref: recovered.ref,
185
207
  stallWindowMs: options.stallWindowMs,
186
208
  });
187
209
  }
@@ -197,14 +219,7 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
197
219
  return harvested;
198
220
  }
199
221
  finally {
200
- if (recoveredChrome && options.closeAfterRecover) {
201
- try {
202
- recoveredChrome.kill();
203
- }
204
- catch {
205
- // best-effort cleanup
206
- }
207
- }
222
+ finishRecoveredChrome(recoveredChrome, options.closeAfterRecover);
208
223
  }
209
224
  }
210
225
  export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
@@ -212,16 +227,19 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
212
227
  if (!meta) {
213
228
  throw new Error(`No session found with ID ${sessionId}.`);
214
229
  }
215
- let endpoint = sessionBrowserEndpoint(meta) ?? {
230
+ const recordedEndpoint = sessionBrowserEndpoint(meta);
231
+ let endpoint = recordedEndpoint ?? {
216
232
  host: DEFAULT_REMOTE_CHROME_HOST,
217
233
  port: DEFAULT_REMOTE_CHROME_PORT,
218
234
  };
219
235
  let browserTabRef = options.browserTabRef ?? resolveSessionTabRef(meta);
220
- const recoverIfMissing = options.recoverIfMissing !== false;
236
+ const recoverIfMissing = options.recoverIfMissing !== false && !options.browserTabRef;
221
237
  let recoveredChrome = null;
222
238
  const stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
223
239
  let lastHash = null;
224
240
  let unchangedSince = Date.now();
241
+ let requireRecoveredContent = false;
242
+ let recoveredContentDeadlineMs = 0;
225
243
  try {
226
244
  // Probe once to see if the live tab is still alive; recover if not.
227
245
  try {
@@ -233,17 +251,19 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
233
251
  }
234
252
  catch (error) {
235
253
  const message = error instanceof Error ? error.message : String(error);
236
- const isMissingTabError = message.includes("No ChatGPT tab matched") ||
237
- message.includes("ECONNREFUSED") ||
238
- message.includes("Could not connect");
239
- if (!isMissingTabError || !recoverIfMissing) {
254
+ if (!isRecoverableMissingTabError(message) || !recoverIfMissing) {
240
255
  throw error;
241
256
  }
242
257
  console.log(chalk.yellow(`No live ChatGPT tab matched session "${sessionId}". Attempting recovery by reopening the saved conversation URL.`));
243
- const recovered = await recoverConversationTab(meta, (line) => console.log(line));
258
+ const recovered = await recoverConversationTab(meta, (line) => console.log(line), {
259
+ existingEndpoint: recordedEndpoint ?? undefined,
260
+ waitForReady: false,
261
+ });
244
262
  recoveredChrome = recovered.chrome;
245
263
  endpoint = { host: recovered.host, port: recovered.port };
246
- browserTabRef = recovered.url;
264
+ browserTabRef = recovered.ref;
265
+ requireRecoveredContent = true;
266
+ recoveredContentDeadlineMs = Date.now() + stallThresholdMs;
247
267
  }
248
268
  while (true) {
249
269
  const harvested = await harvestChatGptTab({
@@ -252,6 +272,14 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
252
272
  ref: browserTabRef,
253
273
  });
254
274
  const fullText = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "";
275
+ if (requireRecoveredContent && !isRecoveredConversationHarvestReady(harvested)) {
276
+ if (Date.now() < recoveredContentDeadlineMs) {
277
+ await new Promise((resolve) => setTimeout(resolve, LIVE_POLL_MS));
278
+ continue;
279
+ }
280
+ throw new Error("Recovered ChatGPT conversation did not become ready in time.");
281
+ }
282
+ requireRecoveredContent = false;
255
283
  const hash = createHash("sha1").update(fullText).digest("hex");
256
284
  if (hash !== lastHash) {
257
285
  lastHash = hash;
@@ -291,13 +319,6 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
291
319
  }
292
320
  }
293
321
  finally {
294
- if (recoveredChrome && options.closeAfterRecover) {
295
- try {
296
- recoveredChrome.kill();
297
- }
298
- catch {
299
- // best-effort cleanup
300
- }
301
- }
322
+ finishRecoveredChrome(recoveredChrome, options.closeAfterRecover);
302
323
  }
303
324
  }
@@ -255,6 +255,20 @@ export function resolveApiModel(modelValue) {
255
255
  // Passthrough for custom/OpenRouter model IDs.
256
256
  return normalized;
257
257
  }
258
+ function parseBrowserGpt56Label(modelValue) {
259
+ const normalized = normalizeModelOption(modelValue).toLowerCase();
260
+ if (!normalized || normalized.includes("/"))
261
+ return null;
262
+ const match = normalized.match(/^(?:(?:chatgpt|gpt)[\s._-]*)?5[._-]6(?:[\s._-]+(.+))?$/);
263
+ if (!match)
264
+ return null;
265
+ return {
266
+ variant: (match[1] ?? "").replace(/[^a-z0-9]+/g, " ").trim(),
267
+ };
268
+ }
269
+ export function isGpt56BrowserLabel(modelValue) {
270
+ return parseBrowserGpt56Label(modelValue) !== null;
271
+ }
258
272
  export function inferModelFromLabel(modelValue) {
259
273
  const normalized = normalizeModelOption(modelValue).toLowerCase();
260
274
  if (!normalized) {
@@ -298,6 +312,16 @@ export function inferModelFromLabel(modelValue) {
298
312
  if (normalized.includes("classic")) {
299
313
  return "gpt-5-pro";
300
314
  }
315
+ // Browser label family currently exposed by ChatGPT as "GPT-5.6 Sol".
316
+ const gpt56Label = parseBrowserGpt56Label(normalized);
317
+ if (gpt56Label) {
318
+ const { variant } = gpt56Label;
319
+ if (!variant)
320
+ return "gpt-5.6";
321
+ if (variant === "sol")
322
+ return "gpt-5.6-sol";
323
+ throw new InvalidArgumentError(`Unknown GPT-5.6 browser variant "${variant}". Use gpt-5.6 or gpt-5.6-sol.`);
324
+ }
301
325
  if (normalized.includes("thinking") && normalized.includes("heavy")) {
302
326
  return "gpt-5.5";
303
327
  }
@@ -25,7 +25,12 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
25
25
  .filter(Boolean);
26
26
  const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
27
27
  const apiModel = resolveApiModel(cliModelArg);
28
- const browserModel = normalizeChatGptModelForBrowser(inferModelFromLabel(cliModelArg));
28
+ // Browser label inference is intentionally engine-scoped: API model ids such as
29
+ // gpt-5.6-luna must remain provider values even though browser mode rejects
30
+ // unrecognized GPT-5.6 picker variants.
31
+ const browserModel = resolvedEngine === "browser"
32
+ ? normalizeChatGptModelForBrowser(inferModelFromLabel(cliModelArg))
33
+ : apiModel;
29
34
  const isCodex = apiModel.startsWith("gpt-5.1-codex");
30
35
  const isClaude = apiModel.startsWith("claude");
31
36
  const isGrok = apiModel.startsWith("grok");
@@ -43,19 +43,42 @@ function formatBytes(bytes) {
43
43
  function isDeepResearchBrowserSession(metadata) {
44
44
  return metadata.mode === "browser" && metadata.browser?.config?.researchMode === "deep";
45
45
  }
46
- function isDeepResearchPlaceholderCapture(metadata, logText) {
47
- const answer = trimBeforeFirstAnswer(logText)
48
- .replace(/^Answer:\s*/i, "")
46
+ const DEEP_RESEARCH_TOOL_CALL_MARKERS = [
47
+ "called tool",
48
+ "used tool",
49
+ "użyto narzędzia",
50
+ "narzędzie wywołane",
51
+ ];
52
+ function isDeepResearchToolCallPlaceholder(answerText, outputTokens) {
53
+ const lines = answerText
49
54
  .toLowerCase()
50
- .replace(/\s+/g, " ")
51
- .trim();
52
- const isToolOnly = answer === "called tool" ||
53
- answer === "used tool" ||
54
- answer === "użyto narzędzia" ||
55
- answer === "narzędzie wywołane";
55
+ .split(/\r?\n/)
56
+ .map((line) => line.replace(/\s+/g, " ").trim())
57
+ .filter(Boolean);
58
+ if (!lines[0] || !DEEP_RESEARCH_TOOL_CALL_MARKERS.includes(lines[0])) {
59
+ return false;
60
+ }
61
+ if (lines.length === 1) {
62
+ return outputTokens == null || outputTokens <= 8;
63
+ }
64
+ const wrapper = lines.slice(1).join(" ");
65
+ const structuralSignals = [
66
+ wrapper.includes("deep research app"),
67
+ /\bcall tool\b/.test(wrapper),
68
+ /\brequest\s*\{/.test(wrapper),
69
+ /\bresponse\s*\{/.test(wrapper),
70
+ /\bsession[_ ]id\b/.test(wrapper),
71
+ ].filter(Boolean).length;
72
+ return wrapper.includes("deep research app") && structuralSignals >= 2;
73
+ }
74
+ export function isDeepResearchPlaceholderCapture(metadata, logText) {
75
+ if (/\[reattach\][^\n]*\nAnswer:/i.test(logText)) {
76
+ return false;
77
+ }
78
+ const answer = trimBeforeFirstAnswer(logText).replace(/^Answer:\s*/i, "");
56
79
  const modelUsage = metadata.models?.find((run) => run.model === metadata.model)?.usage;
57
80
  const outputTokens = metadata.usage?.outputTokens ?? modelUsage?.outputTokens;
58
- return isToolOnly && (outputTokens == null || outputTokens <= 8);
81
+ return isDeepResearchToolCallPlaceholder(answer, outputTokens);
59
82
  }
60
83
  async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
61
84
  const body = result.answerMarkdown || result.answerText;
@@ -566,10 +589,11 @@ export function trimBeforeFirstAnswer(logText) {
566
589
  if (index === -1) {
567
590
  return logText;
568
591
  }
569
- const fromFirstAnswer = logText.slice(index);
570
- if (/^Answer:\s*(called tool|used tool|użyto narzędzia|narzędzie wywołane)\s*\n\[reattach\]/i.test(fromFirstAnswer)) {
571
- const laterIndex = logText.lastIndexOf(marker);
572
- if (laterIndex > index) {
592
+ const laterIndex = logText.lastIndexOf(marker);
593
+ const reattachIndex = logText.indexOf("[reattach]", index + marker.length);
594
+ if (laterIndex > index && reattachIndex > index && reattachIndex < laterIndex) {
595
+ const firstCapture = logText.slice(index + marker.length, reattachIndex);
596
+ if (isDeepResearchToolCallPlaceholder(firstCapture)) {
573
597
  return logText.slice(laterIndex);
574
598
  }
575
599
  }
@@ -469,6 +469,41 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
469
469
  response: { status: "running", incompleteReason: "chrome-disconnected" },
470
470
  });
471
471
  logBrowserReattachGuidance(recoverableRuntime);
472
+ // Only auto-reattach when liveness classified the target as still alive.
473
+ // Closed-Chrome disconnects stay running + guidance but must not enter a
474
+ // futile resume loop (fail closed on availability).
475
+ const recoverableDisconnect = userError.details
476
+ ?.recoverableDisconnect === true;
477
+ if (!recoverableDisconnect) {
478
+ log(dim("Skipping auto-reattach: disconnect classified as non-recoverable."));
479
+ return;
480
+ }
481
+ // Connection-lost should attempt the same recovery path as assistant-timeout.
482
+ // When auto-reattach interval is unset, still try a single resume so a live
483
+ // Chrome/target can be harvested instead of leaving the session permanently running.
484
+ const configuredIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
485
+ const connectionLostIntervalMs = configuredIntervalMs > 0
486
+ ? configuredIntervalMs
487
+ : Math.max(1_000, Math.min(browserConfig?.timeoutMs ?? 30_000, 30_000));
488
+ const success = await autoReattachUntilComplete({
489
+ sessionMeta,
490
+ runtime: recoverableRuntime ?? undefined,
491
+ browserConfig: {
492
+ ...browserConfig,
493
+ autoReattachIntervalMs: connectionLostIntervalMs,
494
+ autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
495
+ autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
496
+ },
497
+ browserMetadata: currentBrowser,
498
+ runOptions,
499
+ modelForStatus,
500
+ notificationSettings,
501
+ log,
502
+ maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
503
+ });
504
+ if (success) {
505
+ return;
506
+ }
472
507
  return;
473
508
  }
474
509
  if (assistantTimeout && mode === "browser" && browserCanReattach) {
@@ -833,7 +868,7 @@ async function writeAssistantOutput(targetPath, content, log) {
833
868
  log(dim(`write-output failed (${reason}); session completed anyway.`));
834
869
  }
835
870
  }
836
- async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, }) {
871
+ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, }) {
837
872
  if (!runtime || !browserConfig) {
838
873
  log(dim("Auto-reattach disabled: missing runtime or browser config."));
839
874
  return false;
@@ -848,11 +883,19 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
848
883
  120_000;
849
884
  const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
850
885
  const maxDeadline = Date.now() + maxTotalMs;
886
+ const attemptLimit = typeof maxAttempts === "number" && maxAttempts > 0
887
+ ? Math.floor(maxAttempts)
888
+ : Number.POSITIVE_INFINITY;
851
889
  if (delayMs > 0) {
852
890
  log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
853
891
  await wait(delayMs);
854
892
  }
855
- log(dim(`Auto-reattach will stop after ${formatElapsed(maxTotalMs)} if no answer is captured.`));
893
+ if (Number.isFinite(attemptLimit)) {
894
+ log(dim(`Auto-reattach will try up to ${attemptLimit} attempt(s).`));
895
+ }
896
+ else {
897
+ log(dim(`Auto-reattach will stop after ${formatElapsed(maxTotalMs)} if no answer is captured.`));
898
+ }
856
899
  const logger = ((message) => {
857
900
  if (message) {
858
901
  log(dim(message));
@@ -943,6 +986,10 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
943
986
  const message = error instanceof Error ? error.message : String(error);
944
987
  log(dim(`Auto-reattach attempt ${attempt} failed: ${message}`));
945
988
  }
989
+ if (attempt >= attemptLimit) {
990
+ log(dim(`Auto-reattach stopped after ${attempt} attempt(s) without capturing an answer.`));
991
+ return false;
992
+ }
946
993
  const remainingAfterAttemptMs = maxDeadline - Date.now();
947
994
  if (remainingAfterAttemptMs <= 0) {
948
995
  log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
@@ -26,7 +26,32 @@ const countTokensAnthropic = (input) => {
26
26
  countTokensAnthropicImpl ??= require("@anthropic-ai/tokenizer").countTokens;
27
27
  return countTokensAnthropicImpl(stringifyTokenizerInput(input));
28
28
  };
29
+ // GPT-5.6 applies higher rates to requests above 272K input tokens. Keep the
30
+ // supported limit at the base-rate boundary until cost estimation supports tiers.
31
+ const GPT_5_6_BASE_RATE_INPUT_LIMIT = 272_000;
29
32
  export const MODEL_CONFIGS = {
33
+ "gpt-5.6": {
34
+ model: "gpt-5.6",
35
+ provider: "openai",
36
+ tokenizer: countTokensGpt5,
37
+ inputLimit: GPT_5_6_BASE_RATE_INPUT_LIMIT,
38
+ pricing: {
39
+ inputPerToken: 5 / 1_000_000,
40
+ outputPerToken: 30 / 1_000_000,
41
+ },
42
+ reasoning: { effort: "xhigh" },
43
+ },
44
+ "gpt-5.6-sol": {
45
+ model: "gpt-5.6-sol",
46
+ provider: "openai",
47
+ tokenizer: countTokensGpt5,
48
+ inputLimit: GPT_5_6_BASE_RATE_INPUT_LIMIT,
49
+ pricing: {
50
+ inputPerToken: 5 / 1_000_000,
51
+ outputPerToken: 30 / 1_000_000,
52
+ },
53
+ reasoning: { effort: "xhigh" },
54
+ },
30
55
  "gpt-5.5-pro": {
31
56
  model: "gpt-5.5-pro",
32
57
  provider: "openai",
@@ -3,6 +3,8 @@ const MODEL_ID_MAP = {
3
3
  "gemini-3.1-pro": "gemini-3.1-pro-preview",
4
4
  "gemini-3.5-flash": "gemini-3.5-flash",
5
5
  "gemini-3-pro": "gemini-3-pro-preview",
6
+ "gpt-5.6": "gpt-5.6",
7
+ "gpt-5.6-sol": "gpt-5.6-sol",
6
8
  "gpt-5.5": "gpt-5.5",
7
9
  "gpt-5.5-pro": "gpt-5.5-pro",
8
10
  "gpt-5.4": "gpt-5.4",
@@ -189,6 +189,9 @@ export async function createRemoteServer(options = {}, deps = {}) {
189
189
  }
190
190
  });
191
191
  automationLogger.verbose = Boolean(payload.options.verbose);
192
+ // Preserve an explicit request to leave the completed conversation tab
193
+ // open before the service forces `keepBrowser` for process lifetime.
194
+ const clientRequestedKeepBrowser = payload.browserConfig?.keepBrowser === true;
192
195
  // Remote runs always rely on the host's own Chrome profile; ignore any inline cookie transfer.
193
196
  if (payload.browserConfig) {
194
197
  payload.browserConfig.inlineCookies = null;
@@ -212,6 +215,11 @@ export async function createRemoteServer(options = {}, deps = {}) {
212
215
  attachments,
213
216
  fallbackSubmission,
214
217
  config: payload.browserConfig,
218
+ // `keepBrowser` above preserves the authenticated shared Chrome
219
+ // process. This separate service policy closes only a successfully
220
+ // captured tab owned by this run, preventing one renderer leak per
221
+ // request while incomplete/reattachable tabs remain untouched.
222
+ closeOwnedTabOnComplete: Boolean(options.manualLoginDefault && !clientRequestedKeepBrowser),
215
223
  log: automationLogger,
216
224
  heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
217
225
  verbose: payload.options.verbose,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@steipete/oracle",
3
- "version": "0.15.2",
4
- "description": "CLI wrapper around OpenAI Responses API with GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
3
+ "version": "0.16.1",
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",
7
7
  "bugs": {
@@ -32,12 +32,12 @@
32
32
  "docs:list": "tsx scripts/docs-list.ts",
33
33
  "docs:check": "node --no-deprecation --import tsx bin/oracle-cli.ts docs check",
34
34
  "docs:site": "node scripts/build-docs-site.mjs",
35
- "build": "tsgo -p tsconfig.build.json && pnpm run build:vendor",
35
+ "build": "tsc -p tsconfig.build.json && pnpm run build:vendor",
36
36
  "build:vendor": "node -e \"const fs=require('fs'); const path=require('path'); const vendorRoot=path.join('dist','vendor'); fs.rmSync(vendorRoot,{recursive:true,force:true}); const vendors=[['oracle-notifier']]; vendors.forEach(([name])=>{const src=path.join('vendor',name); const dest=path.join(vendorRoot,name); fs.mkdirSync(dest,{recursive:true}); if(fs.existsSync(src)){fs.cpSync(src,dest,{recursive:true,force:true});}});\"",
37
37
  "start": "pnpm run build && node ./dist/scripts/run-cli.js",
38
38
  "oracle": "pnpm start",
39
39
  "check": "pnpm run format:check && pnpm run lint",
40
- "typecheck": "tsgo --noEmit",
40
+ "typecheck": "tsc --noEmit",
41
41
  "format": "oxfmt --write .",
42
42
  "format:check": "oxfmt --check .",
43
43
  "format:diff": "oxfmt --write . && git --no-pager diff",
@@ -58,14 +58,14 @@
58
58
  },
59
59
  "dependencies": {
60
60
  "@anthropic-ai/tokenizer": "^0.0.4",
61
- "@google/genai": "^2.10.0",
61
+ "@google/genai": "^2.13.0",
62
62
  "@google/generative-ai": "^0.24.1",
63
63
  "@modelcontextprotocol/sdk": "^1.29.0",
64
64
  "@steipete/sweet-cookie": "^0.4.0",
65
65
  "chalk": "^5.6.2",
66
66
  "chrome-launcher": "^1.2.1",
67
67
  "chrome-remote-interface": "^0.34.0",
68
- "clipboardy": "^5.3.1",
68
+ "clipboardy": "^5.3.2",
69
69
  "commander": "^15.0.0",
70
70
  "dotenv": "^17.4.2",
71
71
  "fast-glob": "^3.3.3",
@@ -74,7 +74,7 @@
74
74
  "json5": "^2.2.3",
75
75
  "kleur": "^4.1.5",
76
76
  "markdansi": "0.3.2",
77
- "openai": "^6.45.0",
77
+ "openai": "^6.48.0",
78
78
  "osc-progress": "^0.3.2",
79
79
  "qs": "^6.15.3",
80
80
  "shiki": "^4.3.1",
@@ -86,17 +86,16 @@
86
86
  "@anthropic-ai/tokenizer": "^0.0.4",
87
87
  "@types/chrome-remote-interface": "^0.34.0",
88
88
  "@types/inquirer": "^9.0.10",
89
- "@types/node": "^26.1.0",
90
- "@typescript/native-preview": "7.0.0-dev.20260706.1",
89
+ "@types/node": "^26.1.1",
91
90
  "@vitest/coverage-v8": "4.1.10",
92
- "devtools-protocol": "0.0.1656784",
91
+ "devtools-protocol": "0.0.1666840",
93
92
  "es-toolkit": "^1.49.0",
94
93
  "esbuild": "^0.28.1",
95
- "oxfmt": "0.57.0",
96
- "oxlint": "^1.72.0",
94
+ "oxfmt": "0.60.0",
95
+ "oxlint": "^1.75.0",
97
96
  "puppeteer-core": "^25.3.0",
98
- "tsx": "^4.23.0",
99
- "typescript": "^6.0.3",
97
+ "tsx": "^4.23.1",
98
+ "typescript": "^7.0.2",
100
99
  "vitest": "^4.1.10"
101
100
  },
102
101
  "devEngines": {