@steipete/oracle 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +72 -337
  2. package/dist/bin/oracle-cli.js +184 -55
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/docs-site/.nojekyll +0 -0
  5. package/dist/docs-site/CNAME +1 -0
  6. package/dist/docs-site/RELEASING.html +410 -0
  7. package/dist/docs-site/agents.html +374 -0
  8. package/dist/docs-site/anthropic.html +368 -0
  9. package/dist/docs-site/bridge.html +416 -0
  10. package/dist/docs-site/browser-mode.html +594 -0
  11. package/dist/docs-site/chromium-forks.html +347 -0
  12. package/dist/docs-site/cli-reference.html +346 -0
  13. package/dist/docs-site/configuration.html +462 -0
  14. package/dist/docs-site/favicon.svg +14 -0
  15. package/dist/docs-site/followup.html +375 -0
  16. package/dist/docs-site/gemini.html +383 -0
  17. package/dist/docs-site/grok.html +325 -0
  18. package/dist/docs-site/index.html +360 -0
  19. package/dist/docs-site/install.html +335 -0
  20. package/dist/docs-site/linux.html +321 -0
  21. package/dist/docs-site/llms.txt +43 -0
  22. package/dist/docs-site/manual-tests.html +596 -0
  23. package/dist/docs-site/mcp.html +391 -0
  24. package/dist/docs-site/multimodel.html +364 -0
  25. package/dist/docs-site/mythical-pro-agents.html +360 -0
  26. package/dist/docs-site/notifier.html +338 -0
  27. package/dist/docs-site/openai-endpoints.html +410 -0
  28. package/dist/docs-site/openrouter.html +344 -0
  29. package/dist/docs-site/quickstart.html +369 -0
  30. package/dist/docs-site/refactor/ux.html +532 -0
  31. package/dist/docs-site/sessions.html +389 -0
  32. package/dist/docs-site/social-card.png +0 -0
  33. package/dist/docs-site/social-card.svg +79 -0
  34. package/dist/docs-site/spec.html +363 -0
  35. package/dist/docs-site/testing.html +320 -0
  36. package/dist/docs-site/tui-debug.html +326 -0
  37. package/dist/docs-site/windows-work.html +324 -0
  38. package/dist/docs-site/windows.html +320 -0
  39. package/dist/scripts/test-browser.js +1 -25
  40. package/dist/src/browser/actions/modelSelection.js +36 -34
  41. package/dist/src/browser/actions/navigation.js +8 -3
  42. package/dist/src/browser/actions/thinkingTime.js +39 -3
  43. package/dist/src/browser/cdpLiveness.js +78 -0
  44. package/dist/src/browser/chromeLifecycle.js +85 -39
  45. package/dist/src/browser/conversationUrl.js +16 -0
  46. package/dist/src/browser/conversationUrlMonitor.js +2 -4
  47. package/dist/src/browser/cookies.js +6 -4
  48. package/dist/src/browser/index.js +181 -50
  49. package/dist/src/browser/liveTabs.js +2 -2
  50. package/dist/src/browser/modelDisplay.js +67 -0
  51. package/dist/src/browser/reattach.js +14 -1
  52. package/dist/src/browser/reattachHelpers.js +3 -5
  53. package/dist/src/browser/reattachability.js +2 -1
  54. package/dist/src/browser/recoverConversation.js +2 -1
  55. package/dist/src/browser/sessionRunner.js +9 -10
  56. package/dist/src/browser/tabLeaseRegistry.js +9 -3
  57. package/dist/src/browser/wslHost.js +50 -0
  58. package/dist/src/cli/browserConfig.js +31 -11
  59. package/dist/src/cli/browserDefaults.js +4 -1
  60. package/dist/src/cli/detach.js +21 -4
  61. package/dist/src/cli/dryRun.js +13 -2
  62. package/dist/src/cli/engine.js +2 -2
  63. package/dist/src/cli/options.js +4 -0
  64. package/dist/src/cli/sessionDisplay.js +60 -8
  65. package/dist/src/cli/sessionLifecycle.js +2 -1
  66. package/dist/src/cli/sessionRunner.js +159 -62
  67. package/dist/src/cli/sessionTable.js +5 -1
  68. package/dist/src/cli/tui/index.js +12 -4
  69. package/dist/src/duration.js +3 -0
  70. package/dist/src/gemini-web/client.js +19 -1
  71. package/dist/src/oracle/modelResolver.js +8 -1
  72. package/dist/src/oracle/request.js +9 -2
  73. package/dist/src/oracle/run.js +43 -3
  74. package/dist/src/remote/server.js +8 -0
  75. package/dist/src/sessionManager.js +41 -12
  76. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  78. package/package.json +16 -16
  79. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -0,0 +1,50 @@
1
+ import { readFileSync } from "node:fs";
2
+ import os from "node:os";
3
+ export function isWsl() {
4
+ if (process.platform !== "linux")
5
+ return false;
6
+ if (process.env.WSL_DISTRO_NAME)
7
+ return true;
8
+ return os.release().toLowerCase().includes("microsoft");
9
+ }
10
+ export function parseWslResolverHost(resolvConf) {
11
+ for (const line of resolvConf.split("\n")) {
12
+ const match = line.match(/^nameserver\s+([0-9.]+)/);
13
+ if (match?.[1]) {
14
+ return match[1].startsWith("127.") ? "127.0.0.1" : match[1];
15
+ }
16
+ }
17
+ return null;
18
+ }
19
+ export function resolveWslHost() {
20
+ if (!isWsl())
21
+ return null;
22
+ try {
23
+ return parseWslResolverHost(readFileSync("/etc/resolv.conf", "utf8"));
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ export function resolveWslChromeHost(options = {}) {
30
+ const remoteDebugHost = options.remoteDebugHost === undefined
31
+ ? process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST
32
+ : options.remoteDebugHost;
33
+ const wslHostIp = options.wslHostIp === undefined ? process.env.WSL_HOST_IP : options.wslHostIp;
34
+ const override = remoteDebugHost?.trim() || wslHostIp?.trim();
35
+ if (override)
36
+ return override;
37
+ if (options.resolvConf !== undefined) {
38
+ return options.resolvConf === null ? null : parseWslResolverHost(options.resolvConf);
39
+ }
40
+ return resolveWslHost();
41
+ }
42
+ export function resolveWslChromeLaunchRoute(options = {}) {
43
+ const connectHost = resolveWslChromeHost(options);
44
+ const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
45
+ return {
46
+ connectHost,
47
+ debugBindAddress: usePatchedLauncher ? "0.0.0.0" : connectHost,
48
+ usePatchedLauncher,
49
+ };
50
+ }
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import chalk from "chalk";
3
4
  import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
4
5
  import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
5
6
  import { normalizeChatgptUrl } from "../browser/utils.js";
@@ -86,6 +87,7 @@ export async function buildBrowserConfig(options) {
86
87
  const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
87
88
  const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
88
89
  const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
90
+ assertBrowserModelAvailable(options.model, modelStrategy);
89
91
  const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
90
92
  let inline = await resolveInlineCookies({
91
93
  inlineArg: options.browserInlineCookies,
@@ -123,38 +125,38 @@ export async function buildBrowserConfig(options) {
123
125
  url,
124
126
  debugPort: selectBrowserPort(options),
125
127
  timeoutMs: options.browserTimeout
126
- ? parseDuration(options.browserTimeout, DEFAULT_BROWSER_TIMEOUT_MS)
128
+ ? parseBrowserDuration(options.browserTimeout, "--browser-timeout", DEFAULT_BROWSER_TIMEOUT_MS)
127
129
  : undefined,
128
130
  inputTimeoutMs: options.browserInputTimeout
129
- ? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
131
+ ? parseBrowserDuration(options.browserInputTimeout, "--browser-input-timeout", DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
130
132
  : undefined,
131
133
  attachmentTimeoutMs: options.browserAttachmentTimeout
132
- ? parseDuration(options.browserAttachmentTimeout, DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
134
+ ? parseBrowserDuration(options.browserAttachmentTimeout, "--browser-attachment-timeout", DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
133
135
  : undefined,
134
136
  assistantRecheckDelayMs: options.browserRecheckDelay
135
- ? parseDuration(options.browserRecheckDelay, 0)
137
+ ? parseBrowserDuration(options.browserRecheckDelay, "--browser-recheck-delay", 0)
136
138
  : undefined,
137
139
  assistantRecheckTimeoutMs: options.browserRecheckTimeout
138
- ? parseDuration(options.browserRecheckTimeout, DEFAULT_BROWSER_RECHECK_TIMEOUT_MS)
140
+ ? parseBrowserDuration(options.browserRecheckTimeout, "--browser-recheck-timeout", DEFAULT_BROWSER_RECHECK_TIMEOUT_MS)
139
141
  : undefined,
140
142
  reuseChromeWaitMs: options.browserReuseWait
141
- ? parseDuration(options.browserReuseWait, 0)
143
+ ? parseBrowserDuration(options.browserReuseWait, "--browser-reuse-wait", 0)
142
144
  : undefined,
143
145
  profileLockTimeoutMs: options.browserProfileLockTimeout
144
- ? parseDuration(options.browserProfileLockTimeout, 0)
146
+ ? parseBrowserDuration(options.browserProfileLockTimeout, "--browser-profile-lock-timeout", 0)
145
147
  : undefined,
146
148
  maxConcurrentTabs: parseMaxConcurrentTabs(options.browserMaxConcurrentTabs),
147
149
  autoReattachDelayMs: options.browserAutoReattachDelay
148
- ? parseDuration(options.browserAutoReattachDelay, 0)
150
+ ? parseBrowserDuration(options.browserAutoReattachDelay, "--browser-auto-reattach-delay", 0)
149
151
  : undefined,
150
152
  autoReattachIntervalMs: options.browserAutoReattachInterval
151
- ? parseDuration(options.browserAutoReattachInterval, 0)
153
+ ? parseBrowserDuration(options.browserAutoReattachInterval, "--browser-auto-reattach-interval", 0)
152
154
  : undefined,
153
155
  autoReattachTimeoutMs: options.browserAutoReattachTimeout
154
- ? parseDuration(options.browserAutoReattachTimeout, DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS)
156
+ ? parseBrowserDuration(options.browserAutoReattachTimeout, "--browser-auto-reattach-timeout", DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS)
155
157
  : undefined,
156
158
  cookieSyncWaitMs: options.browserCookieWait
157
- ? parseDuration(options.browserCookieWait, 0)
159
+ ? parseBrowserDuration(options.browserCookieWait, "--browser-cookie-wait", 0)
158
160
  : undefined,
159
161
  cookieSync: options.browserNoCookieSync ? false : undefined,
160
162
  cookieNames,
@@ -178,6 +180,17 @@ export async function buildBrowserConfig(options) {
178
180
  archiveConversations: options.browserArchive,
179
181
  };
180
182
  }
183
+ function assertBrowserModelAvailable(model, modelStrategy) {
184
+ if (modelStrategy !== "select")
185
+ return;
186
+ const normalized = normalizeChatGptModelForBrowser(model);
187
+ if (normalized !== "gpt-5.2" &&
188
+ normalized !== "gpt-5.2-instant" &&
189
+ normalized !== "gpt-5.2-thinking") {
190
+ return;
191
+ }
192
+ throw new Error(`Browser model "${model}" is retired because ChatGPT no longer offers GPT-5.2 base, Instant, or Thinking. Choose a current GPT-5.5/GPT-5.6 browser model, use --browser-model-strategy current to keep ChatGPT's active model, or use --engine api to retain the GPT-5.2 API alias.`);
193
+ }
181
194
  function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies, }) {
182
195
  if (!attachRunning) {
183
196
  return;
@@ -218,6 +231,13 @@ function parseMaxConcurrentTabs(raw) {
218
231
  }
219
232
  return Math.trunc(value);
220
233
  }
234
+ function parseBrowserDuration(raw, optionName, fallbackMs) {
235
+ const parsed = parseDuration(raw, Number.NaN);
236
+ if (Number.isFinite(parsed))
237
+ return parsed;
238
+ console.log(chalk.yellow(`Warning: invalid ${optionName} duration "${raw}"; using fallback ${fallbackMs}ms.`));
239
+ return fallbackMs;
240
+ }
221
241
  export function mapModelToBrowserLabel(model) {
222
242
  const normalized = normalizeChatGptModelForBrowser(model);
223
243
  // Iterate ordered array to find first match (most specific first)
@@ -11,6 +11,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
11
11
  };
12
12
  const attachRunningRequested = options.browserAttachRunning === true ||
13
13
  (isUnset("browserAttachRunning") && browser.attachRunning === true);
14
+ const currentModelRequestedByCli = options.browserModelStrategy === "current" && getSource("browserModelStrategy") === "cli";
14
15
  const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
15
16
  const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
16
17
  if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
@@ -89,7 +90,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
89
90
  if (isUnset("browserModelStrategy") && browser.modelStrategy !== undefined) {
90
91
  options.browserModelStrategy = browser.modelStrategy;
91
92
  }
92
- if (isUnset("browserThinkingTime") && browser.thinkingTime !== undefined) {
93
+ if (!currentModelRequestedByCli &&
94
+ isUnset("browserThinkingTime") &&
95
+ browser.thinkingTime !== undefined) {
93
96
  options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
94
97
  }
95
98
  if (isUnset("browserResearch") && browser.researchMode !== undefined) {
@@ -1,14 +1,31 @@
1
1
  import { isProModel } from "../oracle/modelResolver.js";
2
2
  export function shouldDetachSession({
3
3
  // Params kept for policy tweaks.
4
- engine, model, waitPreference, disableDetachEnv, }) {
4
+ engine, model, reasoningMode, waitPreference, disableDetachEnv, }) {
5
5
  if (disableDetachEnv)
6
6
  return false;
7
- // Explicit --wait means "stay attached", regardless of model defaults.
7
+ // Keep long local browser Pro work in a separate process even while the CLI
8
+ // stays attached to its session log. If the foreground stream is interrupted,
9
+ // the worker can still finish the browser run and persist the answer.
10
+ if (engine === "browser" && isProModel(model))
11
+ return true;
12
+ // For API runs, explicit --wait keeps execution in the foreground.
8
13
  if (waitPreference)
9
14
  return false;
10
- // Only Pro-tier API runs should start detached by default; browser runs stay inline so failures surface.
11
- if (isProModel(model) && engine === "api")
15
+ // Pro-tier API runs start detached by default.
16
+ if ((isProModel(model) || reasoningMode === "pro") && engine === "api")
12
17
  return true;
13
18
  return false;
14
19
  }
20
+ export function stopDetachedWorker(workerPid, kill = process.kill) {
21
+ try {
22
+ kill(workerPid, "SIGTERM");
23
+ return true;
24
+ }
25
+ catch (error) {
26
+ if (error && typeof error === "object" && "code" in error && error.code === "ESRCH") {
27
+ return false;
28
+ }
29
+ throw error;
30
+ }
31
+ }
@@ -5,6 +5,7 @@ import { assembleBrowserPrompt } 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";
8
+ import { formatBrowserModelTarget } from "../browser/modelDisplay.js";
8
9
  export async function runDryRunSummary({ engine, runOptions, cwd, version, log, browserConfig, }, deps = {}) {
9
10
  if (engine === "browser") {
10
11
  await runBrowserDryRun({ runOptions, cwd, version, log, browserConfig }, deps);
@@ -45,7 +46,12 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig,
45
46
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
46
47
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
47
48
  const suffix = buildTokenEstimateSuffix(artifacts);
48
- const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
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}.`;
49
55
  log(chalk.cyan(headerLine));
50
56
  logBrowserControlPlan(browserConfig, log, "dry-run");
51
57
  logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
@@ -95,7 +101,12 @@ export async function runBrowserPreview({ runOptions, cwd, version, previewMode,
95
101
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
96
102
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
97
103
  const suffix = buildTokenEstimateSuffix(artifacts);
98
- const headerLine = `[preview] Oracle (${version}) browser mode (${runOptions.model}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
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}.`;
99
110
  log(chalk.cyan(headerLine));
100
111
  logBrowserControlPlan(browserConfig, log, "preview");
101
112
  logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
@@ -1,7 +1,7 @@
1
1
  import { isProModel } from "../oracle/modelResolver.js";
2
- export function defaultWaitPreference(model, engine) {
2
+ export function defaultWaitPreference(model, engine, reasoningMode) {
3
3
  // Pro-class API runs can take a long time; prefer non-blocking unless explicitly overridden.
4
- if (engine === "api" && isProModel(model)) {
4
+ if (engine === "api" && (isProModel(model) || reasoningMode === "pro")) {
5
5
  return false;
6
6
  }
7
7
  return true; // browser or non-pro models are fast enough to block by default
@@ -187,6 +187,10 @@ export function resolveApiModel(modelValue) {
187
187
  if (normalized.includes("/")) {
188
188
  return normalized;
189
189
  }
190
+ const gpt56Label = parseBrowserGpt56Label(normalized);
191
+ if (gpt56Label?.variant.split(" ").includes("pro")) {
192
+ 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.");
193
+ }
190
194
  if (normalized.includes("grok")) {
191
195
  return "grok-4.1";
192
196
  }
@@ -12,6 +12,7 @@ import { estimateTokenCount } from "../browser/utils.js";
12
12
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
13
13
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
14
14
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
15
+ import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
15
16
  const isTty = () => Boolean(process.stdout.isTTY);
16
17
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
17
18
  export const MAX_RENDER_BYTES = 200_000;
@@ -185,6 +186,7 @@ export async function attachSession(sessionId, options) {
185
186
  const isVerbose = Boolean(process.env.ORACLE_VERBOSE_RENDER);
186
187
  const runtime = metadata.browser?.runtime;
187
188
  const controllerAlive = isProcessAlive(runtime?.controllerPid);
189
+ const workerAlive = isProcessAlive(metadata.lifecycle?.workerPid);
188
190
  const hasChromeDisconnect = metadata.response?.incompleteReason === "chrome-disconnected";
189
191
  const hasIncompleteCapture = metadata.response?.incompleteReason === "incomplete-capture";
190
192
  const statusAllowsReattach = metadata.status === "running" ||
@@ -204,6 +206,7 @@ export async function attachSession(sessionId, options) {
204
206
  const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
205
207
  metadata.mode === "browser" &&
206
208
  hasFallbackSessionInfo &&
209
+ !workerAlive &&
207
210
  (hasRecoverableConversation ||
208
211
  runtime?.promptSubmitted ||
209
212
  hasLiveChromeFallback ||
@@ -311,11 +314,17 @@ export async function attachSession(sessionId, options) {
311
314
  const usage = run.usage
312
315
  ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
313
316
  : "";
314
- console.log(`- ${chalk.cyan(run.model)} ${run.status}${usage}`);
317
+ const modelLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
318
+ ? formatSessionBrowserModelWithRequestedKey(metadata, run.model)
319
+ : run.model;
320
+ console.log(`- ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
315
321
  }
316
322
  }
317
323
  else if (metadata.model) {
318
- console.log(`Model: ${metadata.model}`);
324
+ const modelLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
325
+ ? formatSessionBrowserModelWithRequestedKey(metadata)
326
+ : metadata.model;
327
+ console.log(`Model: ${modelLabel}`);
319
328
  }
320
329
  const browserEvidence = formatBrowserEvidence(metadata);
321
330
  if (browserEvidence) {
@@ -391,6 +400,9 @@ export async function attachSession(sessionId, options) {
391
400
  if (summary) {
392
401
  console.log(`\n${chalk.green.bold(summary)}`);
393
402
  }
403
+ if (options?.propagateFailure && metadata.status === "error") {
404
+ process.exitCode = 1;
405
+ }
394
406
  return;
395
407
  }
396
408
  if (wantsRender) {
@@ -493,6 +505,48 @@ export async function attachSession(sessionId, options) {
493
505
  }
494
506
  }
495
507
  }
508
+ if (options?.propagateFailure && latest.status === "error") {
509
+ process.exitCode = 1;
510
+ }
511
+ break;
512
+ }
513
+ const controllerPid = latest.lifecycle?.workerPid ?? latest.browser?.runtime?.controllerPid;
514
+ if (latest.lifecycle?.detached && controllerPid && !isProcessAlive(controllerPid)) {
515
+ const settled = await sessionStore.readSession(sessionId);
516
+ if (!settled) {
517
+ break;
518
+ }
519
+ if (settled.status === "completed" || settled.status === "partial") {
520
+ continue;
521
+ }
522
+ await printNew();
523
+ flushRemainder();
524
+ const message = settled.status === "error"
525
+ ? (settled.errorMessage ?? "Detached worker failed.")
526
+ : "Detached worker exited before the session reached a terminal state.";
527
+ const failure = {
528
+ category: "internal",
529
+ message,
530
+ };
531
+ if (settled.model) {
532
+ await sessionStore.updateModelRun(settled.id, settled.model, {
533
+ status: "error",
534
+ completedAt: new Date().toISOString(),
535
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
536
+ error: failure,
537
+ });
538
+ }
539
+ await sessionStore.updateSession(settled.id, {
540
+ status: "error",
541
+ completedAt: new Date().toISOString(),
542
+ errorMessage: message,
543
+ response: { status: "incomplete", incompleteReason: "incomplete-capture" },
544
+ error: failure,
545
+ });
546
+ console.log(chalk.yellow(`${message} Reattach via: ${settled.lifecycle?.reattachCommand}`));
547
+ if (options?.propagateFailure) {
548
+ process.exitCode = 1;
549
+ }
496
550
  break;
497
551
  }
498
552
  await wait(1000);
@@ -555,11 +609,7 @@ export function formatBrowserEvidence(metadata) {
555
609
  const lines = [];
556
610
  const evidence = browser.modelSelection;
557
611
  if (evidence) {
558
- const requested = evidence.requestedModel ?? "(none)";
559
- const resolved = evidence.resolvedLabel ?? "(unavailable)";
560
- const strategy = evidence.strategy ?? "(default)";
561
- const verified = evidence.verified ? "yes" : "no";
562
- lines.push(`model requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}`);
612
+ lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
563
613
  }
564
614
  for (const warning of browser.warnings ?? []) {
565
615
  lines.push(`warning ${warning.code}: ${warning.message}`);
@@ -821,7 +871,9 @@ export function formatCompletionSummary(metadata, options = {}) {
821
871
  if (!metadata.usage || metadata.elapsedMs == null) {
822
872
  return null;
823
873
  }
824
- const modeLabel = metadata.mode === "browser" ? `${metadata.model ?? "n/a"}[browser]` : (metadata.model ?? "n/a");
874
+ const modeLabel = (metadata.mode ?? metadata.options?.mode) === "browser"
875
+ ? `${resolveSessionBrowserModelDisplayName(metadata)}[browser]`
876
+ : (metadata.model ?? "n/a");
825
877
  const usage = metadata.usage;
826
878
  const cost = resolveSessionCost(metadata);
827
879
  const tokensDisplay = [
@@ -1,9 +1,10 @@
1
- export function buildSessionLifecycle({ engine, detached, reattachCommand, }) {
1
+ export function buildSessionLifecycle({ engine, detached, workerPid, reattachCommand, }) {
2
2
  return {
3
3
  engine,
4
4
  execution: detached ? "background" : "foreground",
5
5
  attached: !detached,
6
6
  detached,
7
+ workerPid,
7
8
  reattachCommand,
8
9
  };
9
10
  }