@steipete/oracle 0.18.0 → 0.20.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 (169) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +76 -76
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +170 -45
  13. package/dist/src/browser/actions/modelSelection.js +91 -4
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +364 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +156 -16
  19. package/dist/src/browser/actions/webSearch.js +96 -0
  20. package/dist/src/browser/attachRunning.js +46 -2
  21. package/dist/src/browser/attachmentValidation.js +53 -0
  22. package/dist/src/browser/cancellation.js +105 -0
  23. package/dist/src/browser/chatgptImages.js +2 -0
  24. package/dist/src/browser/chatgptThrottle.js +104 -0
  25. package/dist/src/browser/chromeLifecycle.js +272 -47
  26. package/dist/src/browser/config.js +14 -1
  27. package/dist/src/browser/detect.js +1 -1
  28. package/dist/src/browser/index.js +525 -274
  29. package/dist/src/browser/liveTabs.js +3 -4
  30. package/dist/src/browser/modelDisplay.js +9 -0
  31. package/dist/src/browser/pageActions.js +2 -2
  32. package/dist/src/browser/profileState.js +114 -9
  33. package/dist/src/browser/projectSourcesRunner.js +83 -15
  34. package/dist/src/browser/prompt.js +272 -113
  35. package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
  36. package/dist/src/browser/reattach.js +31 -4
  37. package/dist/src/browser/recoveryTarget.js +131 -0
  38. package/dist/src/browser/sessionRunner.js +73 -11
  39. package/dist/src/browser/tabLeaseRegistry.js +434 -38
  40. package/dist/src/browser/targetClaim.js +54 -0
  41. package/dist/src/browser/utils.js +20 -2
  42. package/dist/src/cli/browserConfig.js +36 -4
  43. package/dist/src/cli/browserDefaults.js +10 -0
  44. package/dist/src/cli/browserTabs.js +15 -29
  45. package/dist/src/cli/detachedSession.js +69 -0
  46. package/dist/src/cli/dryRun.js +58 -48
  47. package/dist/src/cli/harvestIntegrity.js +140 -0
  48. package/dist/src/cli/options.js +15 -0
  49. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  50. package/dist/src/cli/runOptions.js +19 -4
  51. package/dist/src/cli/serveBrowserConfig.js +24 -0
  52. package/dist/src/cli/sessionDisplay.js +13 -7
  53. package/dist/src/cli/sessionRunner.js +95 -12
  54. package/dist/src/config.js +1 -0
  55. package/dist/src/gemini-web/client.js +67 -21
  56. package/dist/src/gemini-web/executor.js +4 -0
  57. package/dist/src/gemini-web/http.js +15 -0
  58. package/dist/src/gemini-web/models.js +15 -11
  59. package/dist/src/mcp/server.js +11 -14
  60. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  61. package/dist/src/mcp/tools/consult.js +86 -24
  62. package/dist/src/mcp/tools/projectSources.js +4 -6
  63. package/dist/src/mcp/tools/sessionResources.js +1 -1
  64. package/dist/src/mcp/tools/sessions.js +2 -2
  65. package/dist/src/mcp/tools/wait.js +182 -0
  66. package/dist/src/mcp/types.js +8 -1
  67. package/dist/src/oracle/config.js +14 -0
  68. package/dist/src/oracle/errors.js +11 -0
  69. package/dist/src/oracle/geminiModels.js +1 -0
  70. package/dist/src/oracle/providerRoutePlan.js +3 -2
  71. package/dist/src/oracle/run.js +27 -4
  72. package/dist/src/remote/client.js +73 -6
  73. package/dist/src/remote/health.js +6 -2
  74. package/dist/src/remote/runSlots.js +83 -0
  75. package/dist/src/remote/server.js +346 -71
  76. package/dist/src/sessionManager.js +36 -1
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  78. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  79. package/package.json +25 -23
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/dist/bin/oracle.js +0 -683
  83. package/dist/docs-site/.nojekyll +0 -0
  84. package/dist/docs-site/CNAME +0 -1
  85. package/dist/docs-site/RELEASING.html +0 -410
  86. package/dist/docs-site/agents.html +0 -374
  87. package/dist/docs-site/anthropic.html +0 -368
  88. package/dist/docs-site/bridge.html +0 -400
  89. package/dist/docs-site/browser-mode.html +0 -588
  90. package/dist/docs-site/chromium-forks.html +0 -347
  91. package/dist/docs-site/cli-reference.html +0 -346
  92. package/dist/docs-site/configuration.html +0 -436
  93. package/dist/docs-site/favicon.svg +0 -14
  94. package/dist/docs-site/followup.html +0 -368
  95. package/dist/docs-site/gemini.html +0 -379
  96. package/dist/docs-site/grok.html +0 -325
  97. package/dist/docs-site/index.html +0 -360
  98. package/dist/docs-site/install.html +0 -335
  99. package/dist/docs-site/linux.html +0 -321
  100. package/dist/docs-site/llms.txt +0 -43
  101. package/dist/docs-site/manual-tests.html +0 -595
  102. package/dist/docs-site/mcp.html +0 -368
  103. package/dist/docs-site/multimodel.html +0 -364
  104. package/dist/docs-site/mythical-pro-agents.html +0 -360
  105. package/dist/docs-site/notifier.html +0 -338
  106. package/dist/docs-site/openai-endpoints.html +0 -387
  107. package/dist/docs-site/openrouter.html +0 -344
  108. package/dist/docs-site/quickstart.html +0 -369
  109. package/dist/docs-site/refactor/ux.html +0 -532
  110. package/dist/docs-site/sessions.html +0 -388
  111. package/dist/docs-site/social-card.png +0 -0
  112. package/dist/docs-site/social-card.svg +0 -79
  113. package/dist/docs-site/spec.html +0 -363
  114. package/dist/docs-site/testing.html +0 -320
  115. package/dist/docs-site/tui-debug.html +0 -326
  116. package/dist/docs-site/windows-work.html +0 -323
  117. package/dist/docs-site/windows.html +0 -320
  118. package/dist/markdansi/types/index.js +0 -4
  119. package/dist/oracle/bin/oracle-cli.js +0 -472
  120. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  121. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  122. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  123. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  124. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  125. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  126. package/dist/oracle/src/browser/config.js +0 -33
  127. package/dist/oracle/src/browser/constants.js +0 -40
  128. package/dist/oracle/src/browser/cookies.js +0 -210
  129. package/dist/oracle/src/browser/domDebug.js +0 -36
  130. package/dist/oracle/src/browser/index.js +0 -331
  131. package/dist/oracle/src/browser/pageActions.js +0 -5
  132. package/dist/oracle/src/browser/prompt.js +0 -88
  133. package/dist/oracle/src/browser/promptSummary.js +0 -20
  134. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  135. package/dist/oracle/src/browser/types.js +0 -1
  136. package/dist/oracle/src/browser/utils.js +0 -62
  137. package/dist/oracle/src/browserMode.js +0 -1
  138. package/dist/oracle/src/cli/browserConfig.js +0 -44
  139. package/dist/oracle/src/cli/dryRun.js +0 -59
  140. package/dist/oracle/src/cli/engine.js +0 -17
  141. package/dist/oracle/src/cli/errorUtils.js +0 -9
  142. package/dist/oracle/src/cli/help.js +0 -70
  143. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  144. package/dist/oracle/src/cli/options.js +0 -103
  145. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  146. package/dist/oracle/src/cli/rootAlias.js +0 -30
  147. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  148. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  149. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  150. package/dist/oracle/src/heartbeat.js +0 -43
  151. package/dist/oracle/src/oracle/client.js +0 -48
  152. package/dist/oracle/src/oracle/config.js +0 -29
  153. package/dist/oracle/src/oracle/errors.js +0 -101
  154. package/dist/oracle/src/oracle/files.js +0 -220
  155. package/dist/oracle/src/oracle/format.js +0 -33
  156. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  157. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  158. package/dist/oracle/src/oracle/request.js +0 -48
  159. package/dist/oracle/src/oracle/run.js +0 -444
  160. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  161. package/dist/oracle/src/oracle/types.js +0 -1
  162. package/dist/oracle/src/oracle.js +0 -9
  163. package/dist/oracle/src/sessionManager.js +0 -205
  164. package/dist/oracle/src/version.js +0 -39
  165. package/dist/scripts/chrome/browser-tools.js +0 -295
  166. package/dist/src/browser/chromeCookies.js +0 -312
  167. package/dist/src/browser/keytarShim.js +0 -56
  168. package/dist/src/browser/profileSync.js +0 -141
  169. package/dist/src/browser/windowsCookies.js +0 -219
@@ -0,0 +1,50 @@
1
+ import fs from "node:fs/promises";
2
+ import { sessionStore } from "../sessionStore.js";
3
+ import { isRecoveredConversationHarvestReady } from "../browser/recoverConversation.js";
4
+ import { hasOtherLiveBrowserController, matchesOwnedRecoveryTarget, retireRecoveredBrowserTarget, } from "../browser/recoveryTarget.js";
5
+ import { extractConversationIdFromUrl } from "../browser/reattachHelpers.js";
6
+ import { estimateTokenCount } from "../browser/utils.js";
7
+ export async function completeOwnedBrowserHarvest(sessionId, harvested, integrity, logger) {
8
+ if (harvested.state !== "completed" ||
9
+ harvested.stopExists ||
10
+ integrity.status !== "matched" ||
11
+ integrity.explicitTarget)
12
+ return;
13
+ const metadata = await sessionStore.readSession(sessionId);
14
+ if (!metadata?.browser?.runtime?.ownedRecoveryTarget ||
15
+ !isRecoveredConversationHarvestReady(harvested))
16
+ return;
17
+ const runtime = metadata.browser?.runtime;
18
+ const capture = {
19
+ host: harvested.host ?? "",
20
+ port: harvested.port ?? 0,
21
+ targetId: harvested.targetId,
22
+ browserWSEndpoint: runtime?.chromeBrowserWSEndpoint,
23
+ conversationId: harvested.conversationId ?? extractConversationIdFromUrl(harvested.url),
24
+ };
25
+ if (hasOtherLiveBrowserController(metadata) || !matchesOwnedRecoveryTarget(metadata, capture))
26
+ return;
27
+ // Harvest previously persisted only a snippet/hash. Save the full answer before retirement.
28
+ const answer = harvested.lastAssistantMarkdown || harvested.lastAssistantText;
29
+ const paths = await sessionStore.getPaths(sessionId);
30
+ await fs.appendFile(paths.log, `[reattach] harvested assistant response from existing Chrome tab\nAnswer:\n${answer}\n`, "utf8");
31
+ const outputTokens = estimateTokenCount(answer);
32
+ const usage = { inputTokens: 0, outputTokens, reasoningTokens: 0, totalTokens: outputTokens };
33
+ if (metadata.model) {
34
+ await sessionStore.updateModelRun(sessionId, metadata.model, {
35
+ status: "completed",
36
+ completedAt: new Date().toISOString(),
37
+ usage,
38
+ });
39
+ }
40
+ await sessionStore.updateSession(sessionId, {
41
+ status: "completed",
42
+ completedAt: new Date().toISOString(),
43
+ usage,
44
+ errorMessage: undefined,
45
+ error: undefined,
46
+ transport: undefined,
47
+ response: { status: "completed" },
48
+ });
49
+ await retireRecoveredBrowserTarget(sessionId, capture, logger);
50
+ }
@@ -4,7 +4,7 @@ import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBa
4
4
  import { resolveGeminiModelId } from "../oracle/gemini.js";
5
5
  import { resolveOverriddenApiModel } from "../oracle/modelResolver.js";
6
6
  import { PromptValidationError } from "../oracle/errors.js";
7
- import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
7
+ import { normalizeChatGptModelForBrowser, isGpt6ProAlias } from "./browserConfig.js";
8
8
  import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
9
9
  import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
10
10
  export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
@@ -24,7 +24,10 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
24
24
  .map((entry) => normalizeModelOption(entry))
25
25
  .filter(Boolean);
26
26
  const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
27
- const apiModel = resolveApiModel(cliModelArg);
27
+ const isGpt6Pro = isGpt6ProAlias(cliModelArg);
28
+ const apiModel = isGpt6Pro && (resolvedEngine === "browser" || browserEngineRequested)
29
+ ? "gpt-6-pro"
30
+ : resolveApiModel(cliModelArg);
28
31
  // Browser label inference is intentionally engine-scoped: API model ids such as
29
32
  // gpt-5.6-luna must remain provider values even though browser mode rejects
30
33
  // unrecognized GPT-5.6 picker variants.
@@ -36,9 +39,11 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
36
39
  const isGrok = apiModel.startsWith("grok");
37
40
  const engineWasBrowser = resolvedEngine === "browser";
38
41
  const allModels = normalizedRequestedModels.length > 0
39
- ? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
42
+ ? Array.from(new Set(normalizedRequestedModels.map((entry) => isGpt6ProAlias(entry) && (resolvedEngine === "browser" || browserEngineRequested)
43
+ ? "gpt-6-pro"
44
+ : resolveApiModel(entry))))
40
45
  : [apiModel];
41
- const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
46
+ const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel ?? apiModel];
42
47
  const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
43
48
  const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
44
49
  if (hasNonBrowserCompatibleTarget) {
@@ -52,6 +57,16 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
52
57
  const fixedEngine = isCodex || isClaude || isGrok || azureAutoApi || normalizedRequestedModels.length > 0
53
58
  ? "api"
54
59
  : resolvedEngine;
60
+ if (fixedEngine === "api") {
61
+ if (isGpt6ProAlias(cliModelArg)) {
62
+ resolveApiModel(cliModelArg);
63
+ }
64
+ for (const entry of normalizedRequestedModels) {
65
+ if (isGpt6ProAlias(entry)) {
66
+ resolveApiModel(entry);
67
+ }
68
+ }
69
+ }
55
70
  // Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
56
71
  const resolvedModel = fixedEngine === "browser" ? browserModel : apiModel;
57
72
  const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
@@ -0,0 +1,24 @@
1
+ import { resolveBrowserApprovalWait } from "../browser/config.js";
2
+ import { parseRemoteChromeTarget } from "./browserConfig.js";
3
+ import { applyBrowserDefaultsFromConfig } from "./browserDefaults.js";
4
+ export function buildServeBrowserConfig(options, config) {
5
+ const flags = {
6
+ browserAttachRunning: options.browserAttachRunning,
7
+ remoteChrome: options.remoteChrome,
8
+ browserApprovalWait: options.browserApprovalWait ??
9
+ (process.env.ORACLE_BROWSER_APPROVAL_WAIT?.trim() || undefined),
10
+ };
11
+ const browser = config.browser;
12
+ applyBrowserDefaultsFromConfig(flags, {
13
+ browser: {
14
+ attachRunning: browser?.attachRunning,
15
+ remoteChrome: browser?.remoteChrome,
16
+ approvalWaitMs: browser?.approvalWaitMs,
17
+ },
18
+ }, (key) => (flags[key] === undefined ? undefined : "cli"));
19
+ return {
20
+ attachRunning: flags.browserAttachRunning ?? false,
21
+ remoteChrome: flags.remoteChrome ? parseRemoteChromeTarget(flags.remoteChrome) : null,
22
+ approvalWaitMs: resolveBrowserApprovalWait(flags.browserApprovalWait),
23
+ };
24
+ }
@@ -6,13 +6,14 @@ import { formatFinishLine } from "../oracle/finishLine.js";
6
6
  import { sessionStore, wait } from "../sessionStore.js";
7
7
  import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
8
8
  import { resumeBrowserSession } from "../browser/reattach.js";
9
+ import { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
9
10
  import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
10
11
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
11
12
  import { estimateTokenCount } from "../browser/utils.js";
12
13
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
13
14
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
14
15
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
15
- import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
16
+ import { formatBrowserModelSelectionEvidence, formatBrowserThinkingSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
16
17
  const isTty = () => Boolean(process.stdout.isTTY);
17
18
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
18
19
  export const MAX_RENDER_BYTES = 200_000;
@@ -88,11 +89,8 @@ async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
88
89
  await fs.writeFile(paths.log, `[reattach] replaced incomplete Deep Research capture from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
89
90
  return;
90
91
  }
91
- const logWriter = sessionStore.createLogWriter(sessionId);
92
- logWriter.logLine("[reattach] captured assistant response from existing Chrome tab");
93
- logWriter.logLine("Answer:");
94
- logWriter.logLine(body);
95
- logWriter.stream.end();
92
+ const paths = await sessionStore.getPaths(sessionId);
93
+ await fs.appendFile(paths.log, `[reattach] captured assistant response from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
96
94
  }
97
95
  async function saveReattachBrowserArtifacts(sessionId, metadata, result) {
98
96
  const body = result.answerMarkdown || result.answerText;
@@ -255,6 +253,7 @@ export async function attachSession(sessionId, options) {
255
253
  config: metadata.browser?.config,
256
254
  runtime,
257
255
  modelSelection: metadata.browser?.modelSelection,
256
+ thinkingSelection: metadata.browser?.thinkingSelection,
258
257
  warnings: metadata.browser?.warnings,
259
258
  },
260
259
  artifacts,
@@ -263,6 +262,7 @@ export async function attachSession(sessionId, options) {
263
262
  transport: undefined,
264
263
  });
265
264
  console.log(chalk.green("Reattach succeeded; session marked completed."));
265
+ await retireRecoveredBrowserTarget(sessionId, result.captureTarget, (line) => console.log(dim(line)));
266
266
  metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
267
267
  }
268
268
  catch (error) {
@@ -603,7 +603,9 @@ export function formatUserErrorMetadata(metadata) {
603
603
  }
604
604
  export function formatBrowserEvidence(metadata) {
605
605
  const browser = metadata.browser;
606
- if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
606
+ if (!browser?.modelSelection &&
607
+ !browser?.thinkingSelection &&
608
+ (!browser?.warnings || browser.warnings.length === 0)) {
607
609
  return null;
608
610
  }
609
611
  const lines = [];
@@ -611,6 +613,10 @@ export function formatBrowserEvidence(metadata) {
611
613
  if (evidence) {
612
614
  lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
613
615
  }
616
+ const thinkingEvidence = browser.thinkingSelection;
617
+ if (thinkingEvidence) {
618
+ lines.push(`effort ${formatBrowserThinkingSelectionEvidence(thinkingEvidence)}`);
619
+ }
614
620
  for (const warning of browser.warnings ?? []) {
615
621
  lines.push(`warning ${warning.code}: ${warning.message}`);
616
622
  }
@@ -1,4 +1,5 @@
1
1
  import kleur from "kleur";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
@@ -21,8 +22,10 @@ import { sanitizeOscProgress } from "./oscUtils.js";
21
22
  import { readFiles } from "../oracle/files.js";
22
23
  import { cwd as getCwd } from "node:process";
23
24
  import { resumeBrowserSession } from "../browser/reattach.js";
25
+ import { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
24
26
  import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
25
27
  import { estimateTokenCount } from "../browser/utils.js";
28
+ import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
26
29
  import { formatElapsed } from "../oracle/format.js";
27
30
  import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
28
31
  const isTty = process.stdout.isTTY;
@@ -78,7 +81,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
78
81
  cwd,
79
82
  log,
80
83
  }, runnerDeps);
81
- await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
84
+ const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
85
+ const outputArtifacts = await copyBrowserOutputArtifacts({
86
+ outputPath: writtenOutputPath,
87
+ savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
88
+ log,
89
+ });
90
+ const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
82
91
  await sendSessionNotification({
83
92
  sessionId: sessionMeta.id,
84
93
  sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
@@ -105,9 +114,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
105
114
  runtime: result.runtime,
106
115
  archive: result.archive,
107
116
  modelSelection: result.modelSelection,
108
- warnings: result.warnings,
117
+ thinkingSelection: result.thinkingSelection,
118
+ warnings: browserWarnings.length > 0 ? browserWarnings : undefined,
109
119
  },
110
- artifacts: mergeArtifacts(sessionMeta.artifacts, result.artifacts),
120
+ artifacts: mergeArtifacts(sessionMeta.artifacts, mergeArtifacts(result.artifacts, outputArtifacts.artifacts)),
111
121
  response: undefined,
112
122
  transport: undefined,
113
123
  error: undefined,
@@ -848,6 +858,81 @@ function resolveSessionPath(sessionDir, targetPath) {
848
858
  }
849
859
  return path.join(sessionDir, targetPath);
850
860
  }
861
+ async function copyBrowserOutputArtifacts(params) {
862
+ const artifacts = [];
863
+ const warnings = [];
864
+ if (!params.outputPath || !params.savedFiles?.length) {
865
+ return { artifacts, warnings };
866
+ }
867
+ const outputDir = path.dirname(params.outputPath);
868
+ for (const savedFile of params.savedFiles) {
869
+ const filename = sanitizeArtifactFilename(savedFile.filename ?? path.basename(savedFile.path), "artifact.bin");
870
+ let copied;
871
+ try {
872
+ copied = await copyFileWithoutOverwrite(savedFile.path, path.join(outputDir, filename));
873
+ const [stat, sha256] = await Promise.all([fs.stat(copied), computeFileSha256(copied)]);
874
+ if (savedFile.sizeBytes != null && stat.size !== savedFile.sizeBytes) {
875
+ throw new Error(`size mismatch (expected ${savedFile.sizeBytes}, received ${stat.size})`);
876
+ }
877
+ if (savedFile.sha256 && sha256 !== savedFile.sha256) {
878
+ throw new Error(`sha256 mismatch (expected ${savedFile.sha256}, received ${sha256})`);
879
+ }
880
+ const artifact = {
881
+ kind: "file",
882
+ path: copied,
883
+ label: `${savedFile.label ?? filename} (write-output)`,
884
+ mimeType: savedFile.mimeType,
885
+ sizeBytes: stat.size,
886
+ sourceUrl: savedFile.sourceUrl,
887
+ sha256,
888
+ validation: savedFile.validation,
889
+ transfer: { status: "not-needed" },
890
+ origin: { mode: "local" },
891
+ };
892
+ artifacts.push(artifact);
893
+ params.log(dim(`[browser] Saved write-output file artifact to ${copied} sha256=${sha256}`));
894
+ }
895
+ catch (error) {
896
+ if (copied) {
897
+ await fs.unlink(copied).catch(() => undefined);
898
+ }
899
+ const reason = error instanceof Error ? error.message : String(error);
900
+ const message = `Failed to copy saved file artifact ${savedFile.path} beside ${params.outputPath}: ${reason}`;
901
+ params.log(dim(`[browser] ${message}`));
902
+ warnings.push({
903
+ code: "browser-output-artifact-copy-failed",
904
+ severity: "warning",
905
+ message,
906
+ details: {
907
+ sourcePath: savedFile.path,
908
+ outputPath: params.outputPath,
909
+ sha256: savedFile.sha256,
910
+ },
911
+ });
912
+ }
913
+ }
914
+ return { artifacts, warnings };
915
+ }
916
+ async function copyFileWithoutOverwrite(sourcePath, baseTargetPath) {
917
+ const ext = path.extname(baseTargetPath);
918
+ const stem = ext ? path.basename(baseTargetPath, ext) : path.basename(baseTargetPath);
919
+ const dir = path.dirname(baseTargetPath);
920
+ let suffix = 1;
921
+ for (;;) {
922
+ const targetPath = suffix === 1 ? baseTargetPath : path.join(dir, `${stem}-${suffix}${ext}`);
923
+ try {
924
+ await fs.copyFile(sourcePath, targetPath, fsConstants.COPYFILE_EXCL);
925
+ return targetPath;
926
+ }
927
+ catch (error) {
928
+ if (isErrorCode(error, "EEXIST")) {
929
+ suffix += 1;
930
+ continue;
931
+ }
932
+ throw error;
933
+ }
934
+ }
935
+ }
851
936
  async function writeAssistantOutput(targetPath, content, log) {
852
937
  if (!targetPath)
853
938
  return;
@@ -955,11 +1040,8 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
955
1040
  existingArtifacts: sessionMeta.artifacts,
956
1041
  logger,
957
1042
  });
958
- const logWriter = sessionStore.createLogWriter(sessionMeta.id);
959
- logWriter.logLine(`[auto-reattach] captured assistant response on attempt ${attempt}`);
960
- logWriter.logLine("Answer:");
961
- logWriter.logLine(answerText);
962
- logWriter.stream.end();
1043
+ const paths = await sessionStore.getPaths(sessionMeta.id);
1044
+ await fs.appendFile(paths.log, `[auto-reattach] captured assistant response on attempt ${attempt}\nAnswer:\n${answerText}\n`, "utf8");
963
1045
  if (modelForStatus) {
964
1046
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
965
1047
  status: "completed",
@@ -1005,6 +1087,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
1005
1087
  transport: undefined,
1006
1088
  });
1007
1089
  log(kleur.green("Auto-reattach succeeded; session marked completed."));
1090
+ await retireRecoveredBrowserTarget(sessionMeta.id, result.captureTarget, logger);
1008
1091
  return true;
1009
1092
  }
1010
1093
  catch (error) {
@@ -1057,11 +1140,11 @@ export function deriveModelOutputPath(basePath, model) {
1057
1140
  const suffix = ext.length > 0 ? `${stem}.${model}${ext}` : `${stem}.${model}`;
1058
1141
  return path.join(dir, suffix);
1059
1142
  }
1143
+ function isErrorCode(error, expected) {
1144
+ return error instanceof Error && error.code === expected;
1145
+ }
1060
1146
  function isPermissionError(error) {
1061
- if (!(error instanceof Error))
1062
- return false;
1063
- const code = error.code;
1064
- return code === "EACCES" || code === "EPERM";
1147
+ return isErrorCode(error, "EACCES") || isErrorCode(error, "EPERM");
1065
1148
  }
1066
1149
  function buildFallbackPath(original) {
1067
1150
  const ext = path.extname(original);
@@ -135,6 +135,7 @@ function sanitizeProjectConfig(config) {
135
135
  "attachRunning",
136
136
  "timeoutMs",
137
137
  "inputTimeoutMs",
138
+ "approvalWaitMs",
138
139
  "attachmentTimeoutMs",
139
140
  "assistantRecheckDelayMs",
140
141
  "assistantRecheckTimeoutMs",
@@ -1,7 +1,9 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, } from "./models.js";
4
- const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
4
+ import { fetchGeminiWebResource } from "./http.js";
5
+ import { buildGeminiWebModelHeader, FALLBACK_GEMINI_WEB_MODEL, getGeminiWebModelSelection, } from "./models.js";
6
+ const USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36";
5
7
  const MODEL_HEADER_NAME = "x-goog-ext-525001261-jspb";
6
8
  const GEMINI_APP_URL = "https://gemini.google.com/app";
7
9
  const GEMINI_STREAM_GENERATE_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate";
@@ -61,7 +63,7 @@ function buildCookieHeader(cookieMap) {
61
63
  }
62
64
  export async function fetchGeminiAccessToken(cookieMap, signal) {
63
65
  const cookieHeader = buildCookieHeader(cookieMap);
64
- const res = await fetch(GEMINI_APP_URL, {
66
+ const res = await fetchGeminiWebResource(GEMINI_APP_URL, {
65
67
  redirect: "follow",
66
68
  signal,
67
69
  headers: {
@@ -112,7 +114,7 @@ function ensureFullSizeImageUrl(url) {
112
114
  async function fetchWithCookiePreservingRedirects(url, init, signal, maxRedirects = 10) {
113
115
  let current = url;
114
116
  for (let i = 0; i <= maxRedirects; i += 1) {
115
- const res = await fetch(current, { ...init, redirect: "manual", signal });
117
+ const res = await fetchGeminiWebResource(current, { ...init, redirect: "manual", signal });
116
118
  if (res.status >= 300 && res.status < 400) {
117
119
  const location = res.headers.get("location");
118
120
  if (!location)
@@ -146,7 +148,7 @@ async function uploadGeminiFile(filePath, signal) {
146
148
  const mimeType = resolveGeminiUploadMimeType(absPath);
147
149
  const form = new FormData();
148
150
  form.append("file", new Blob([data], { type: mimeType }), fileName);
149
- const res = await fetch(GEMINI_UPLOAD_URL, {
151
+ const res = await fetchGeminiWebResource(GEMINI_UPLOAD_URL, {
150
152
  method: "POST",
151
153
  redirect: "follow",
152
154
  signal,
@@ -162,18 +164,38 @@ async function uploadGeminiFile(filePath, signal) {
162
164
  }
163
165
  return { id: text, name: fileName, mimeType };
164
166
  }
165
- function buildGeminiFReqPayload(prompt, uploaded, chatMetadata) {
166
- const promptPayload = uploaded.length > 0
167
- ? [
168
- prompt,
169
- 0,
170
- null,
171
- // Format: [[[fileId, 1, null, "mimeType"], "filename", ...]]
172
- uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
173
- ]
174
- : [prompt];
175
- const innerList = [promptPayload, null, chatMetadata ?? null];
176
- return JSON.stringify([null, JSON.stringify(innerList)]);
167
+ function buildGeminiFReqPayload(prompt, uploaded, chatMetadata, model, requestId) {
168
+ const request = Array(97).fill(null);
169
+ request[0] = [
170
+ prompt,
171
+ 0,
172
+ null,
173
+ uploaded.map((file) => [[file.id, 1, null, file.mimeType], file.name]),
174
+ null,
175
+ null,
176
+ 0,
177
+ ];
178
+ request[1] = ["en"];
179
+ request[2] = chatMetadata ?? ["", "", "", null, null, null, null, null, null, ""];
180
+ request[6] = [1];
181
+ request[7] = 1;
182
+ request[10] = 1;
183
+ request[11] = 0;
184
+ request[17] = [[0]];
185
+ request[18] = 0;
186
+ request[27] = 1;
187
+ request[30] = [4];
188
+ request[41] = [1];
189
+ request[53] = 0;
190
+ request[59] = requestId;
191
+ request[61] = [];
192
+ request[68] = 2;
193
+ const selection = getGeminiWebModelSelection(model);
194
+ request[79] = selection.modelCode;
195
+ request[80] = selection.thinkingCode;
196
+ request[91] = 0;
197
+ request[96] = chatMetadata == null ? 1 : 0;
198
+ return JSON.stringify([null, JSON.stringify(request)]);
177
199
  }
178
200
  export function parseGeminiStreamGenerateResponse(rawText) {
179
201
  const responseJson = JSON.parse(trimGeminiJsonEnvelope(rawText));
@@ -274,11 +296,12 @@ export async function runGeminiWebOnce(input) {
274
296
  }
275
297
  uploaded.push(await uploadGeminiFile(file, input.signal));
276
298
  }
277
- const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null);
299
+ const requestId = randomUUID().toUpperCase();
300
+ const fReq = buildGeminiFReqPayload(input.prompt, uploaded, input.chatMetadata ?? null, input.model, requestId);
278
301
  const params = new URLSearchParams();
279
302
  params.set("at", at);
280
303
  params.set("f.req", fReq);
281
- const res = await fetch(GEMINI_STREAM_GENERATE_URL, {
304
+ const res = await fetchGeminiWebResource(GEMINI_STREAM_GENERATE_URL, {
282
305
  method: "POST",
283
306
  redirect: "follow",
284
307
  signal: input.signal,
@@ -290,6 +313,9 @@ export async function runGeminiWebOnce(input) {
290
313
  "user-agent": USER_AGENT,
291
314
  cookie: cookieHeader,
292
315
  [MODEL_HEADER_NAME]: buildGeminiWebModelHeader(input.model),
316
+ "x-goog-ext-73010989-jspb": "[0]",
317
+ "x-goog-ext-73010990-jspb": "[0]",
318
+ "x-goog-ext-525005358-jspb": JSON.stringify([requestId, 1]),
293
319
  },
294
320
  body: params.toString(),
295
321
  });
@@ -338,10 +364,30 @@ export async function runGeminiWebOnce(input) {
338
364
  export async function runGeminiWebWithFallback(input) {
339
365
  const attempt = await runGeminiWebOnce(input);
340
366
  if (isGeminiModelUnavailable(attempt.errorCode) && input.model !== FALLBACK_GEMINI_WEB_MODEL) {
367
+ if (input.allowModelFallback === false) {
368
+ throw new Error(`Requested Gemini web model ${input.model} is unavailable and model fallback is disabled.`);
369
+ }
341
370
  const fallback = await runGeminiWebOnce({ ...input, model: FALLBACK_GEMINI_WEB_MODEL });
342
- return { ...fallback, effectiveModel: FALLBACK_GEMINI_WEB_MODEL };
371
+ return {
372
+ ...assertGeminiWebRunSucceeded(fallback),
373
+ effectiveModel: FALLBACK_GEMINI_WEB_MODEL,
374
+ };
375
+ }
376
+ return { ...assertGeminiWebRunSucceeded(attempt), effectiveModel: input.model };
377
+ }
378
+ function assertGeminiWebRunSucceeded(output) {
379
+ if (output.errorCode !== undefined) {
380
+ throw new Error(`Gemini web request failed with error code ${output.errorCode}.`);
381
+ }
382
+ if (output.errorMessage) {
383
+ throw new Error(`Gemini web request failed: ${output.errorMessage}`);
384
+ }
385
+ if (!output.text.trim() &&
386
+ output.images.length === 0 &&
387
+ extractGgdlUrls(output.rawResponseText).length === 0) {
388
+ throw new Error("Gemini web request ended without a response.");
343
389
  }
344
- return { ...attempt, effectiveModel: input.model };
390
+ return output;
345
391
  }
346
392
  export async function saveFirstGeminiImageFromOutput(output, cookieMap, outputPath, signal) {
347
393
  const generatedOrWeb = output.images.find((img) => img.kind === "generated") ?? output.images[0];
@@ -325,6 +325,7 @@ export function createGeminiWebExecutor(geminiOptions) {
325
325
  cookieMap: cookieResult.cookieMap,
326
326
  chatMetadata: null,
327
327
  signal: controller.signal,
328
+ allowModelFallback: geminiOptions.allowModelFallback,
328
329
  });
329
330
  const editPrompt = `Use image generation tool to ${prompt}`;
330
331
  const out = await runGeminiWebWithFallback({
@@ -334,6 +335,7 @@ export function createGeminiWebExecutor(geminiOptions) {
334
335
  cookieMap: cookieResult.cookieMap,
335
336
  chatMetadata: intro.metadata,
336
337
  signal: controller.signal,
338
+ allowModelFallback: geminiOptions.allowModelFallback,
337
339
  });
338
340
  response = {
339
341
  text: out.text ?? null,
@@ -357,6 +359,7 @@ export function createGeminiWebExecutor(geminiOptions) {
357
359
  cookieMap: cookieResult.cookieMap,
358
360
  chatMetadata: null,
359
361
  signal: controller.signal,
362
+ allowModelFallback: geminiOptions.allowModelFallback,
360
363
  });
361
364
  response = {
362
365
  text: out.text ?? null,
@@ -379,6 +382,7 @@ export function createGeminiWebExecutor(geminiOptions) {
379
382
  cookieMap: cookieResult.cookieMap,
380
383
  chatMetadata: null,
381
384
  signal: controller.signal,
385
+ allowModelFallback: geminiOptions.allowModelFallback,
382
386
  });
383
387
  response = {
384
388
  text: out.text ?? null,
@@ -0,0 +1,15 @@
1
+ export async function fetchGeminiWebResource(url, init = {}) {
2
+ try {
3
+ return await fetch(url, init);
4
+ }
5
+ catch (error) {
6
+ const cause = error instanceof Error ? error.cause : undefined;
7
+ if (cause &&
8
+ typeof cause === "object" &&
9
+ "code" in cause &&
10
+ cause.code === "UND_ERR_HEADERS_OVERFLOW") {
11
+ throw new Error("Gemini response headers exceed Node's configured limit. Start Oracle with NODE_OPTIONS=--max-http-header-size=65536 (preserving any existing Node options) and retry.", { cause: error });
12
+ }
13
+ throw error;
14
+ }
15
+ }
@@ -6,29 +6,29 @@ const MODEL_SPECS = {
6
6
  hash: "1d44b34bcaa1c04d",
7
7
  modelCode: 6,
8
8
  thinkingCode: 1,
9
+ capacity: 2,
9
10
  },
10
11
  "gemini-3.5-flash": {
11
12
  hash: "56fdd199312815e2",
12
13
  modelCode: 1,
13
14
  thinkingCode: 1,
15
+ capacity: 2,
14
16
  },
15
17
  "gemini-3.1-pro": {
16
- hash: "797f3d0293f288ad",
18
+ hash: "e6fa609c3fa255c0",
17
19
  modelCode: 3,
18
20
  thinkingCode: 1,
21
+ capacity: 2,
19
22
  },
20
23
  "gemini-3-pro-deep-think": {
21
- hash: "797f3d0293f288ad",
22
- modelCode: 3,
23
- thinkingCode: 3,
24
+ hash: "e051ce1aa80aa576",
25
+ modelCode: 5,
26
+ thinkingCode: 2,
27
+ capacity: 2,
24
28
  },
25
29
  };
26
- let clientId;
27
- function getGeminiWebClientId() {
28
- clientId ??= randomUUID().toUpperCase();
29
- return clientId;
30
- }
31
- export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClientId()) {
30
+ const clientId = randomUUID().toUpperCase();
31
+ export function buildGeminiWebModelHeader(model, webClientId = clientId) {
32
32
  const spec = MODEL_SPECS[model];
33
33
  return JSON.stringify([
34
34
  1,
@@ -42,7 +42,7 @@ export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClien
42
42
  [4, 5, 6, 8],
43
43
  null,
44
44
  null,
45
- 3,
45
+ spec.capacity,
46
46
  null,
47
47
  null,
48
48
  spec.modelCode,
@@ -50,6 +50,10 @@ export function buildGeminiWebModelHeader(model, webClientId = getGeminiWebClien
50
50
  webClientId,
51
51
  ]);
52
52
  }
53
+ export function getGeminiWebModelSelection(model) {
54
+ const { modelCode, thinkingCode } = MODEL_SPECS[model];
55
+ return { modelCode, thinkingCode };
56
+ }
53
57
  export function resolveGeminiWebModel(desiredModel, log) {
54
58
  const desired = typeof desiredModel === "string" ? desiredModel.trim() : "";
55
59
  if (!desired)