@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
@@ -1,6 +1,22 @@
1
1
  export { parseDuration } from "../duration.js";
2
- export function delay(ms) {
3
- return new Promise((resolve) => setTimeout(resolve, ms));
2
+ import { currentBrowserAbortSignal } from "./cancellation.js";
3
+ export function delay(ms, signal = currentBrowserAbortSignal()) {
4
+ return new Promise((resolve, reject) => {
5
+ if (signal?.aborted) {
6
+ reject(signal.reason);
7
+ return;
8
+ }
9
+ const timer = setTimeout(() => {
10
+ signal?.removeEventListener("abort", onAbort);
11
+ resolve();
12
+ }, ms);
13
+ const onAbort = () => {
14
+ clearTimeout(timer);
15
+ signal?.removeEventListener("abort", onAbort);
16
+ reject(signal?.reason);
17
+ };
18
+ signal?.addEventListener("abort", onAbort, { once: true });
19
+ });
4
20
  }
5
21
  export function estimateTokenCount(text) {
6
22
  if (!text) {
@@ -18,6 +34,8 @@ export async function withRetries(task, options = {}) {
18
34
  return await task();
19
35
  }
20
36
  catch (error) {
37
+ if (error instanceof Error && ["AbortError", "BrowserRunCancelledError"].includes(error.name))
38
+ throw error;
21
39
  if (attempt === retries) {
22
40
  throw error;
23
41
  }
@@ -5,6 +5,7 @@ import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
5
5
  import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
6
6
  import { normalizeChatgptUrl } from "../browser/utils.js";
7
7
  import { parseDuration } from "../duration.js";
8
+ import { resolveBrowserApprovalWait } from "../browser/config.js";
8
9
  import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
9
10
  import { getOracleHomeDir } from "../oracleHome.js";
10
11
  const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
@@ -23,6 +24,10 @@ const CURRENT_CHATGPT_PRO_ALIASES = new Set([
23
24
  // The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
24
25
  const BROWSER_MODEL_LABELS = [
25
26
  // Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
27
+ // GPT-6 (Astra) has no entry of its own in the ChatGPT picker: it is the "Latest" radio of the
28
+ // advanced view, and "GPT-6 Pro" is that radio with the power slider at Pro (composer pill "6 Pro").
29
+ ["gpt-6-pro", "Latest"],
30
+ ["gpt-6-astra", "Latest"],
26
31
  ["gpt-5.6-sol", "GPT-5.6 Sol"],
27
32
  ["gpt-5.6", "GPT-5.6 Sol"],
28
33
  ["gpt-5.5-pro", "GPT-5.5"],
@@ -46,6 +51,13 @@ const BROWSER_MODEL_LABELS = [
46
51
  ];
47
52
  export function normalizeChatGptModelForBrowser(model) {
48
53
  const normalized = model.toLowerCase();
54
+ // Browser-only alias: gpt-6-pro keeps its name so the Pro tier default survives (label "Latest").
55
+ if (isGpt6ProAlias(normalized)) {
56
+ return "gpt-6-pro";
57
+ }
58
+ if (isGpt6Alias(normalized)) {
59
+ return "gpt-6-astra";
60
+ }
49
61
  if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
50
62
  return model;
51
63
  }
@@ -71,6 +83,18 @@ export function normalizeChatGptModelForBrowser(model) {
71
83
  }
72
84
  return model;
73
85
  }
86
+ // Documented spellings only: gpt-6, gpt-6-astra, gpt-6-pro (plus their label forms such as
87
+ // "GPT-6 Pro") and "latest" map to ChatGPT's "Latest" model. Any other gpt-6-* id (gpt-6-codex,
88
+ // gpt-6-custom, ...) is not an alias and must pass through unchanged for custom/OpenRouter use.
89
+ const GPT6_ALIAS_PATTERN = /^gpt[-_ ]?6(?:[-_ ](?:astra|pro))?$/;
90
+ const GPT6_PRO_ALIAS_PATTERN = /^gpt[-_ ]?6[-_ ]pro$/;
91
+ export function isGpt6Alias(model) {
92
+ const normalized = model?.trim().toLowerCase() ?? "";
93
+ return normalized === "latest" || GPT6_ALIAS_PATTERN.test(normalized);
94
+ }
95
+ export function isGpt6ProAlias(model) {
96
+ return GPT6_PRO_ALIAS_PATTERN.test(model?.trim().toLowerCase() ?? "");
97
+ }
74
98
  export function isCurrentChatGptProAlias(model) {
75
99
  return CURRENT_CHATGPT_PRO_ALIASES.has(model?.trim().toLowerCase() ?? "");
76
100
  }
@@ -79,7 +103,10 @@ export function resolveDefaultBrowserThinkingTime({ model, requestedModel, model
79
103
  if (strategy !== "select")
80
104
  return undefined;
81
105
  const normalizedModel = normalizeChatGptModelForBrowser(model);
82
- return isCurrentChatGptProAlias(requestedModel ?? model) || normalizedModel === "gpt-5.5-pro"
106
+ return isCurrentChatGptProAlias(requestedModel ?? model) ||
107
+ isGpt6ProAlias(requestedModel ?? model) ||
108
+ normalizedModel === "gpt-6-pro" ||
109
+ normalizedModel === "gpt-5.5-pro"
83
110
  ? "pro"
84
111
  : undefined;
85
112
  }
@@ -153,6 +180,7 @@ export async function buildBrowserConfig(options) {
153
180
  inputTimeoutMs: options.browserInputTimeout
154
181
  ? parseBrowserDuration(options.browserInputTimeout, "--browser-input-timeout", DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
155
182
  : undefined,
183
+ approvalWaitMs: resolveBrowserApprovalWait(options.browserApprovalWait),
156
184
  attachmentTimeoutMs: options.browserAttachmentTimeout
157
185
  ? parseBrowserDuration(options.browserAttachmentTimeout, "--browser-attachment-timeout", DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
158
186
  : undefined,
@@ -206,7 +234,9 @@ export async function buildBrowserConfig(options) {
206
234
  remoteChrome,
207
235
  browserTabRef: options.browserTab ?? undefined,
208
236
  thinkingTime,
209
- researchMode: options.browserResearch === "deep" ? "deep" : "off",
237
+ researchMode: options.browserResearch === "deep" || options.browserResearch === "search"
238
+ ? options.browserResearch
239
+ : "off",
210
240
  archiveConversations: options.browserArchive,
211
241
  };
212
242
  }
@@ -286,12 +316,14 @@ export function resolveBrowserModelLabel(input, model) {
286
316
  return mapModelToBrowserLabel(model);
287
317
  }
288
318
  const normalizedInput = trimmed.toLowerCase();
289
- if (normalizedInput === model.toLowerCase()) {
319
+ if (normalizedInput === model.toLowerCase() ||
320
+ (isGpt6Alias(normalizedInput) && (model === "gpt-6-astra" || model === "gpt-6-pro")) ||
321
+ (isGpt6ProAlias(normalizedInput) && model === "gpt-6-pro")) {
290
322
  return mapModelToBrowserLabel(model);
291
323
  }
292
324
  return trimmed;
293
325
  }
294
- function parseRemoteChromeTarget(raw) {
326
+ export function parseRemoteChromeTarget(raw) {
295
327
  const target = raw.trim();
296
328
  if (!target) {
297
329
  throw new Error("Invalid remote-chrome value: expected host:port but received an empty string.");
@@ -12,6 +12,13 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
12
12
  const attachRunningRequested = options.browserAttachRunning === true ||
13
13
  (isUnset("browserAttachRunning") && browser.attachRunning === true);
14
14
  const currentModelRequestedByCli = options.browserModelStrategy === "current" && getSource("browserModelStrategy") === "cli";
15
+ if (!options.copyProfile &&
16
+ isUnset("remoteChrome") &&
17
+ options.remoteChrome === undefined &&
18
+ browser.remoteChrome) {
19
+ const { host, port } = browser.remoteChrome;
20
+ options.remoteChrome = `${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
21
+ }
15
22
  const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
16
23
  const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
17
24
  if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
@@ -45,6 +52,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
45
52
  if (isUnset("browserInputTimeout") && typeof browser.inputTimeoutMs === "number") {
46
53
  options.browserInputTimeout = String(browser.inputTimeoutMs);
47
54
  }
55
+ if (isUnset("browserApprovalWait") && typeof browser.approvalWaitMs === "number") {
56
+ options.browserApprovalWait = String(browser.approvalWaitMs);
57
+ }
48
58
  if (isUnset("browserAttachmentTimeout") && typeof browser.attachmentTimeoutMs === "number") {
49
59
  options.browserAttachmentTimeout = String(browser.attachmentTimeoutMs);
50
60
  }
@@ -2,9 +2,11 @@ import fs from "node:fs/promises";
2
2
  import { createHash } from "node:crypto";
3
3
  import chalk from "chalk";
4
4
  import { sessionStore } from "../sessionStore.js";
5
- import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, extractConversationIdFromUrl, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
5
+ import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
6
6
  import { isRecoveredConversationHarvestReady, recoverConversationTab, } from "../browser/recoverConversation.js";
7
7
  import { resolveOutputPath } from "./writeOutputPath.js";
8
+ import { persistBrowserHarvest } from "./harvestIntegrity.js";
9
+ import { completeOwnedBrowserHarvest } from "./recoveredBrowserHarvest.js";
8
10
  const LIVE_POLL_MS = 2000;
9
11
  const DEFAULT_STALL_THRESHOLD_MS = 60_000;
10
12
  function isRecoverableMissingTabError(message) {
@@ -85,35 +87,17 @@ function resolveSessionTabRef(meta) {
85
87
  export function resolveSessionTabRefForTest(meta) {
86
88
  return resolveSessionTabRef(meta);
87
89
  }
88
- async function persistHarvest(sessionId, meta, harvested) {
89
- const hash = createHash("sha1")
90
- .update(harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "")
91
- .digest("hex");
92
- const browser = {
93
- ...(meta.browser ?? {}),
94
- harvest: {
95
- targetId: harvested.targetId,
96
- url: harvested.url,
97
- conversationId: harvested.conversationId ?? extractConversationIdFromUrl(harvested.url),
98
- harvestedAt: new Date().toISOString(),
99
- assistantHash: hash,
100
- state: harvested.state,
101
- stopExists: harvested.stopExists,
102
- sendExists: harvested.sendExists,
103
- assistantCount: harvested.assistantCount,
104
- currentModelLabel: harvested.currentModelLabel,
105
- lastAssistantSnippet: harvested.lastAssistantSnippet,
106
- },
107
- };
108
- await sessionStore.updateSession(sessionId, { browser });
109
- }
110
- function printHarvestSummary(sessionId, harvested) {
90
+ function printHarvestSummary(sessionId, harvested, integrity) {
111
91
  console.log(chalk.bold(`Session: ${sessionId}`));
112
92
  console.log(`Target: ${harvested.targetId}`);
113
93
  console.log(`State: ${formatBrowserTabState(harvested)}`);
114
94
  console.log(`Model: ${harvested.currentModelLabel || "(unknown)"}`);
115
95
  console.log(`URL: ${harvested.url}`);
116
96
  console.log(`Assistant turns: ${harvested.assistantCount}`);
97
+ console.log(`Capture identity: ${integrity.status}${integrity.explicitTarget ? " (explicit target)" : ""}`);
98
+ if (integrity.status === "mismatch") {
99
+ console.log(chalk.yellow("Explicit harvest target differs from the saved capture; original artifacts are unchanged."));
100
+ }
117
101
  console.log(`Signals: stop=${harvested.stopExists ? "yes" : "no"} send=${harvested.sendExists ? "yes" : "no"}`);
118
102
  if (harvested.lastUserSnippet) {
119
103
  console.log(`Last user: ${harvested.lastUserSnippet}`);
@@ -207,8 +191,8 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
207
191
  stallWindowMs: options.stallWindowMs,
208
192
  });
209
193
  }
210
- await persistHarvest(sessionId, meta, harvested);
211
- printHarvestSummary(sessionId, harvested);
194
+ const integrity = await persistBrowserHarvest(sessionId, harvested, Boolean(options.browserTabRef));
195
+ printHarvestSummary(sessionId, harvested, integrity);
212
196
  const output = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "";
213
197
  if (options.writeOutputPath) {
214
198
  await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
@@ -216,6 +200,7 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
216
200
  if (!options.quietOutput && output) {
217
201
  process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
218
202
  }
203
+ await completeOwnedBrowserHarvest(sessionId, harvested, integrity, (line) => console.log(line));
219
204
  return harvested;
220
205
  }
221
206
  finally {
@@ -287,8 +272,8 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
287
272
  const statusLine = `[${new Date().toISOString()}] state=${harvested.state} stop=${harvested.stopExists ? "yes" : "no"} ` +
288
273
  `send=${harvested.sendExists ? "yes" : "no"} model=${harvested.currentModelLabel || "(unknown)"} ` +
289
274
  `snippet=${snippet(harvested.lastAssistantSnippet || fullText, 160)}`;
275
+ await persistBrowserHarvest(sessionId, harvested, Boolean(options.browserTabRef));
290
276
  console.log(statusLine);
291
- await persistHarvest(sessionId, meta, harvested);
292
277
  }
293
278
  const derivedState = harvested.stopExists
294
279
  ? Date.now() - unchangedSince >= stallThresholdMs
@@ -304,8 +289,8 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
304
289
  ...harvested,
305
290
  state: derivedState,
306
291
  };
307
- await persistHarvest(sessionId, meta, finalHarvest);
308
- printHarvestSummary(sessionId, finalHarvest);
292
+ const integrity = await persistBrowserHarvest(sessionId, finalHarvest, Boolean(options.browserTabRef));
293
+ printHarvestSummary(sessionId, finalHarvest, integrity);
309
294
  const output = finalHarvest.lastAssistantMarkdown ?? finalHarvest.lastAssistantText ?? "";
310
295
  if (options.writeOutputPath) {
311
296
  await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
@@ -313,6 +298,7 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
313
298
  if (output) {
314
299
  process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
315
300
  }
301
+ await completeOwnedBrowserHarvest(sessionId, finalHarvest, integrity, (line) => console.log(line));
316
302
  return finalHarvest;
317
303
  }
318
304
  await new Promise((resolve) => setTimeout(resolve, LIVE_POLL_MS));
@@ -0,0 +1,69 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ export function resolveOracleCliEntrypoint(moduleUrl = import.meta.url) {
4
+ const extension = fileURLToPath(moduleUrl).endsWith(".ts") ? "ts" : "js";
5
+ return fileURLToPath(new URL(`../../bin/oracle-cli.${extension}`, moduleUrl));
6
+ }
7
+ export function buildDetachedSessionSpawnSpec({ sessionId, cliEntrypoint = resolveOracleCliEntrypoint(), env = process.env, nodeExecutable = process.execPath, }) {
8
+ return {
9
+ command: nodeExecutable,
10
+ args: ["--", cliEntrypoint, "--exec-session", sessionId],
11
+ options: {
12
+ detached: true,
13
+ stdio: ["pipe", "ignore", "ignore"],
14
+ env: {
15
+ ...env,
16
+ ORACLE_DETACHED_START_GATE: "1",
17
+ },
18
+ windowsHide: true,
19
+ },
20
+ };
21
+ }
22
+ export function launchDetachedSession({ sessionId, cliEntrypoint, env, nodeExecutable, prepare, spawnProcess = spawn, }) {
23
+ return new Promise((resolve, reject) => {
24
+ let child;
25
+ try {
26
+ const spec = buildDetachedSessionSpawnSpec({
27
+ sessionId,
28
+ cliEntrypoint,
29
+ env,
30
+ nodeExecutable,
31
+ });
32
+ child = spawnProcess(spec.command, spec.args, spec.options);
33
+ }
34
+ catch (error) {
35
+ reject(error);
36
+ return;
37
+ }
38
+ child.once("error", reject);
39
+ child.once("spawn", async () => {
40
+ if (child.pid === undefined) {
41
+ child.kill();
42
+ reject(new Error("Detached session worker started without a process ID."));
43
+ return;
44
+ }
45
+ try {
46
+ await prepare(child.pid);
47
+ if (!child.stdin) {
48
+ throw new Error("Detached session worker started without a writable start gate.");
49
+ }
50
+ const startGate = child.stdin;
51
+ await new Promise((resolveGate, rejectGate) => {
52
+ startGate.once("error", rejectGate);
53
+ startGate.end("ready\n", (error) => {
54
+ if (error)
55
+ rejectGate(error);
56
+ else
57
+ resolveGate();
58
+ });
59
+ });
60
+ child.unref();
61
+ resolve(child.pid);
62
+ }
63
+ catch (error) {
64
+ child.kill();
65
+ reject(error);
66
+ }
67
+ });
68
+ });
69
+ }
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from "../oracle.js";
3
3
  import { isKnownModel } from "../oracle/modelResolver.js";
4
- import { assembleBrowserPrompt } from "../browser/prompt.js";
4
+ import { assembleBrowserPrompt, cleanupGeneratedBrowserBundles, } from "../browser/prompt.js";
5
5
  import { buildTokenEstimateSuffix, formatAttachmentLabel } from "../browser/promptSummary.js";
6
6
  import { buildCookiePlan } from "../browser/policies.js";
7
7
  import { describeBrowserControlPlan, formatBrowserControlPlan } from "../browser/controlPlan.js";
@@ -45,19 +45,24 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig,
45
45
  validateBrowserFollowUps(runOptions, browserConfig);
46
46
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
47
47
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
48
- const suffix = buildTokenEstimateSuffix(artifacts);
49
- const displayModel = formatBrowserModelTarget({
50
- model: runOptions.model,
51
- desiredModel: browserConfig?.desiredModel,
52
- modelStrategy: browserConfig?.modelStrategy,
53
- });
54
- const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
55
- log(chalk.cyan(headerLine));
56
- logBrowserControlPlan(browserConfig, log, "dry-run");
57
- logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
58
- logBrowserCookieStrategy(browserConfig, log, "dry-run");
59
- logBrowserArchivePolicy(browserConfig, log, "dry-run");
60
- logBrowserFileSummary(artifacts, log, "dry-run");
48
+ try {
49
+ const suffix = buildTokenEstimateSuffix(artifacts);
50
+ const displayModel = formatBrowserModelTarget({
51
+ model: runOptions.model,
52
+ desiredModel: browserConfig?.desiredModel,
53
+ modelStrategy: browserConfig?.modelStrategy,
54
+ });
55
+ const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
56
+ log(chalk.cyan(headerLine));
57
+ logBrowserControlPlan(browserConfig, log, "dry-run");
58
+ logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
59
+ logBrowserCookieStrategy(browserConfig, log, "dry-run");
60
+ logBrowserArchivePolicy(browserConfig, log, "dry-run");
61
+ logBrowserFileSummary(artifacts, log, "dry-run");
62
+ }
63
+ finally {
64
+ await cleanupGeneratedBrowserBundles(artifacts);
65
+ }
61
66
  }
62
67
  function logBrowserControlPlan(browserConfig, log, label) {
63
68
  const plan = describeBrowserControlPlan(browserConfig);
@@ -100,42 +105,47 @@ export async function runBrowserPreview({ runOptions, cwd, version, previewMode,
100
105
  validateBrowserFollowUps(runOptions, browserConfig);
101
106
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
102
107
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
103
- const suffix = buildTokenEstimateSuffix(artifacts);
104
- const displayModel = formatBrowserModelTarget({
105
- model: runOptions.model,
106
- desiredModel: browserConfig?.desiredModel,
107
- modelStrategy: browserConfig?.modelStrategy,
108
- });
109
- const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
110
- log(chalk.cyan(headerLine));
111
- logBrowserControlPlan(browserConfig, log, "preview");
112
- logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
113
- logBrowserCookieStrategy(browserConfig, log, "preview");
114
- logBrowserFileSummary(artifacts, log, "preview");
115
- if (previewMode === "json" || previewMode === "full") {
116
- const attachmentSummary = artifacts.attachments.map((attachment) => ({
117
- path: attachment.path,
118
- displayPath: attachment.displayPath,
119
- sizeBytes: attachment.sizeBytes,
120
- }));
121
- const previewPayload = {
108
+ try {
109
+ const suffix = buildTokenEstimateSuffix(artifacts);
110
+ const displayModel = formatBrowserModelTarget({
122
111
  model: runOptions.model,
123
- engine: "browser",
124
- composerText: artifacts.composerText,
125
- attachments: attachmentSummary,
126
- inlineFileCount: artifacts.inlineFileCount,
127
- bundled: artifacts.bundled,
128
- tokenEstimate: artifacts.estimatedInputTokens,
129
- browserFollowUps: runOptions.browserFollowUps ?? [],
130
- };
131
- log("");
132
- log(chalk.bold("Preview JSON"));
133
- log(JSON.stringify(previewPayload, null, 2));
112
+ desiredModel: browserConfig?.desiredModel,
113
+ modelStrategy: browserConfig?.modelStrategy,
114
+ });
115
+ const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
116
+ log(chalk.cyan(headerLine));
117
+ logBrowserControlPlan(browserConfig, log, "preview");
118
+ logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
119
+ logBrowserCookieStrategy(browserConfig, log, "preview");
120
+ logBrowserFileSummary(artifacts, log, "preview");
121
+ if (previewMode === "json" || previewMode === "full") {
122
+ const attachmentSummary = artifacts.attachments.map((attachment) => ({
123
+ path: attachment.path,
124
+ displayPath: attachment.displayPath,
125
+ sizeBytes: attachment.sizeBytes,
126
+ }));
127
+ const previewPayload = {
128
+ model: runOptions.model,
129
+ engine: "browser",
130
+ composerText: artifacts.composerText,
131
+ attachments: attachmentSummary,
132
+ inlineFileCount: artifacts.inlineFileCount,
133
+ bundled: artifacts.bundled,
134
+ tokenEstimate: artifacts.estimatedInputTokens,
135
+ browserFollowUps: runOptions.browserFollowUps ?? [],
136
+ };
137
+ log("");
138
+ log(chalk.bold("Preview JSON"));
139
+ log(JSON.stringify(previewPayload, null, 2));
140
+ }
141
+ if (previewMode === "full") {
142
+ log("");
143
+ log(chalk.bold("Composer Text"));
144
+ log(artifacts.composerText || chalk.dim("(empty prompt)"));
145
+ }
134
146
  }
135
- if (previewMode === "full") {
136
- log("");
137
- log(chalk.bold("Composer Text"));
138
- log(artifacts.composerText || chalk.dim("(empty prompt)"));
147
+ finally {
148
+ await cleanupGeneratedBrowserBundles(artifacts);
139
149
  }
140
150
  }
141
151
  function logBrowserFollowUpSummary(followUps, log, label) {
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { sessionStore } from "../sessionStore.js";
6
+ import { extractConversationIdFromUrl } from "../browser/liveTabs.js";
7
+ import { BrowserAutomationError } from "../oracle/errors.js";
8
+ const INTEGRITY_WARNING = "browser-harvest-integrity";
9
+ const INTEGRITY_MESSAGE = "Harvest or saved conversation identities conflict. Original artifacts were preserved; inspect browser.harvest.integrity before using the answer.";
10
+ async function readTranscriptConversation(filename) {
11
+ let file;
12
+ try {
13
+ const stat = await fs.lstat(filename);
14
+ if (!stat.isFile() || stat.isSymbolicLink())
15
+ return undefined;
16
+ file = await fs.open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
17
+ if (!(await file.stat()).isFile())
18
+ return undefined;
19
+ const buffer = Buffer.alloc(2_048);
20
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
21
+ const match = buffer
22
+ .subarray(0, bytesRead)
23
+ .toString("utf8")
24
+ .match(/^# Oracle Browser Transcript\r?\n\r?\nConversation: ([^\r\n]+)\r?\n/);
25
+ return match ? extractConversationIdFromUrl(match[1]) : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ finally {
31
+ await file?.close();
32
+ }
33
+ }
34
+ export async function persistBrowserHarvest(sessionId, harvested, explicitTarget = false) {
35
+ // Harvesting can take time; compare against the latest saved capture, not the initial read.
36
+ const meta = await sessionStore.readSession(sessionId);
37
+ if (!meta)
38
+ throw new Error(`No session found with ID ${sessionId}.`);
39
+ const captured = [];
40
+ const unverifiedSources = [];
41
+ const add = (source, id) => {
42
+ if (id)
43
+ captured.push({ source, conversationId: id });
44
+ };
45
+ const addUrl = (source, url, expectsConversation = true) => {
46
+ if (!url)
47
+ return;
48
+ const id = extractConversationIdFromUrl(url);
49
+ add(source, id);
50
+ if (!id && expectsConversation)
51
+ unverifiedSources.push(source);
52
+ };
53
+ add("runtime", meta.browser?.runtime?.conversationId);
54
+ addUrl("runtime-url", meta.browser?.runtime?.tabUrl);
55
+ addUrl("archive", meta.browser?.archive?.conversationUrl);
56
+ for (const [index, artifact] of (meta.artifacts ?? []).entries()) {
57
+ addUrl(`artifact:${index}`, artifact.sourceUrl, artifact.kind === "transcript" || artifact.kind === "deep-research-report");
58
+ }
59
+ const artifactDir = path.join((await sessionStore.getPaths(sessionId)).dir, "artifacts");
60
+ const directoryStat = await fs.lstat(artifactDir).catch(() => null);
61
+ if (directoryStat?.isDirectory() && !directoryStat.isSymbolicLink()) {
62
+ const transcripts = new Map([[path.join(artifactDir, "transcript.md"), false]]);
63
+ for (const artifact of meta.artifacts ?? []) {
64
+ if (artifact.kind !== "transcript")
65
+ continue;
66
+ const candidate = path.isAbsolute(artifact.path)
67
+ ? artifact.path
68
+ : path.resolve(artifactDir, "..", artifact.path);
69
+ // Stored paths must remain direct children of this session's artifact directory.
70
+ if (path.dirname(candidate) === artifactDir)
71
+ transcripts.set(candidate, true);
72
+ else
73
+ unverifiedSources.push("external-transcript-path");
74
+ }
75
+ for (const [filename, expected] of transcripts) {
76
+ const source = `transcript:${path.basename(filename)}`;
77
+ const id = await readTranscriptConversation(filename);
78
+ add(source, id);
79
+ if (!id && (expected || (await fs.lstat(filename).catch(() => null)))) {
80
+ unverifiedSources.push(source);
81
+ }
82
+ }
83
+ }
84
+ else if (directoryStat || meta.artifacts?.some((artifact) => artifact.kind === "transcript")) {
85
+ unverifiedSources.push("artifact-directory");
86
+ }
87
+ const observedConversationId = harvested.conversationId ?? extractConversationIdFromUrl(harvested.url);
88
+ const capturedIds = new Set(captured.map((entry) => entry.conversationId));
89
+ const previousIntegrity = meta.browser?.harvest?.integrity;
90
+ const mismatch = capturedIds.size > 1 ||
91
+ (observedConversationId
92
+ ? captured.some((entry) => entry.conversationId !== observedConversationId)
93
+ : previousIntegrity?.status === "mismatch");
94
+ const integrity = {
95
+ status: mismatch
96
+ ? "mismatch"
97
+ : observedConversationId && captured.length && unverifiedSources.length === 0
98
+ ? "matched"
99
+ : "unverified",
100
+ observedConversationId,
101
+ captured,
102
+ unverifiedSources,
103
+ explicitTarget,
104
+ previousHarvestConversationId: meta.browser?.harvest?.conversationId ?? previousIntegrity?.previousHarvestConversationId,
105
+ };
106
+ const warnings = (meta.browser?.warnings ?? []).filter((warning) => warning.code !== INTEGRITY_WARNING);
107
+ if (mismatch) {
108
+ warnings.push({ code: INTEGRITY_WARNING, severity: "warning", message: INTEGRITY_MESSAGE });
109
+ }
110
+ await sessionStore.updateSession(sessionId, {
111
+ browser: {
112
+ ...meta.browser,
113
+ warnings,
114
+ harvest: {
115
+ targetId: harvested.targetId,
116
+ url: harvested.url,
117
+ conversationId: observedConversationId,
118
+ harvestedAt: new Date().toISOString(),
119
+ assistantHash: createHash("sha1")
120
+ .update(harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "")
121
+ .digest("hex"),
122
+ state: harvested.state,
123
+ stopExists: harvested.stopExists,
124
+ sendExists: harvested.sendExists,
125
+ assistantCount: harvested.assistantCount,
126
+ currentModelLabel: harvested.currentModelLabel,
127
+ lastAssistantSnippet: harvested.lastAssistantSnippet,
128
+ integrity,
129
+ },
130
+ },
131
+ });
132
+ if (mismatch && !explicitTarget) {
133
+ throw new BrowserAutomationError(INTEGRITY_MESSAGE, {
134
+ stage: "harvest-integrity",
135
+ code: "conversation-identity-mismatch",
136
+ integrity,
137
+ });
138
+ }
139
+ return integrity;
140
+ }
@@ -1,3 +1,4 @@
1
+ import { isGpt6Alias, isGpt6ProAlias } from "./browserConfig.js";
1
2
  import { InvalidArgumentError } from "commander";
2
3
  import { parseDuration } from "../duration.js";
3
4
  import path from "node:path";
@@ -187,6 +188,12 @@ export function resolveApiModel(modelValue) {
187
188
  if (normalized.includes("/")) {
188
189
  return normalized;
189
190
  }
191
+ if (isGpt6ProAlias(normalized)) {
192
+ throw new InvalidArgumentError("GPT-6 Pro is an API reasoning mode, not a model slug. Use --model gpt-6-astra --reasoning-mode pro (or --engine browser --model gpt-6-pro).");
193
+ }
194
+ if (isGpt6Alias(normalized)) {
195
+ return "gpt-6-astra";
196
+ }
190
197
  const gpt56Label = parseBrowserGpt56Label(normalized);
191
198
  if (gpt56Label?.variant.split(" ").includes("pro")) {
192
199
  throw new InvalidArgumentError("GPT-5.6 Pro is an API reasoning mode, not a model slug. Use --model gpt-5.6-sol --reasoning-mode pro.");
@@ -284,6 +291,14 @@ export function inferModelFromLabel(modelValue) {
284
291
  if (normalized.includes("/")) {
285
292
  return normalized;
286
293
  }
294
+ // gpt-6 / gpt-6-pro / latest: ChatGPT's "Latest" model (GPT-6 Astra). The browser-only -pro alias
295
+ // is passed through so its Pro tier default survives (resolveDefaultBrowserThinkingTime).
296
+ if (isGpt6ProAlias(normalized)) {
297
+ return "gpt-6-pro";
298
+ }
299
+ if (isGpt6Alias(normalized)) {
300
+ return "gpt-6-astra";
301
+ }
287
302
  if (normalized.includes("grok")) {
288
303
  return "grok-4.1";
289
304
  }