@steipete/oracle 0.16.1 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +72 -337
  2. package/dist/bin/oracle-cli.js +185 -56
  3. package/dist/docs-site/.nojekyll +0 -0
  4. package/dist/docs-site/CNAME +1 -0
  5. package/dist/docs-site/RELEASING.html +410 -0
  6. package/dist/docs-site/agents.html +374 -0
  7. package/dist/docs-site/anthropic.html +368 -0
  8. package/dist/docs-site/bridge.html +416 -0
  9. package/dist/docs-site/browser-mode.html +594 -0
  10. package/dist/docs-site/chromium-forks.html +347 -0
  11. package/dist/docs-site/cli-reference.html +346 -0
  12. package/dist/docs-site/configuration.html +462 -0
  13. package/dist/docs-site/favicon.svg +14 -0
  14. package/dist/docs-site/followup.html +375 -0
  15. package/dist/docs-site/gemini.html +383 -0
  16. package/dist/docs-site/grok.html +325 -0
  17. package/dist/docs-site/index.html +360 -0
  18. package/dist/docs-site/install.html +335 -0
  19. package/dist/docs-site/linux.html +321 -0
  20. package/dist/docs-site/llms.txt +43 -0
  21. package/dist/docs-site/manual-tests.html +596 -0
  22. package/dist/docs-site/mcp.html +391 -0
  23. package/dist/docs-site/multimodel.html +364 -0
  24. package/dist/docs-site/mythical-pro-agents.html +360 -0
  25. package/dist/docs-site/notifier.html +338 -0
  26. package/dist/docs-site/openai-endpoints.html +410 -0
  27. package/dist/docs-site/openrouter.html +344 -0
  28. package/dist/docs-site/quickstart.html +369 -0
  29. package/dist/docs-site/refactor/ux.html +532 -0
  30. package/dist/docs-site/sessions.html +389 -0
  31. package/dist/docs-site/social-card.png +0 -0
  32. package/dist/docs-site/social-card.svg +79 -0
  33. package/dist/docs-site/spec.html +363 -0
  34. package/dist/docs-site/testing.html +320 -0
  35. package/dist/docs-site/tui-debug.html +326 -0
  36. package/dist/docs-site/windows-work.html +324 -0
  37. package/dist/docs-site/windows.html +320 -0
  38. package/dist/scripts/test-browser.js +1 -25
  39. package/dist/src/browser/actions/modelSelection.js +36 -34
  40. package/dist/src/browser/actions/navigation.js +8 -3
  41. package/dist/src/browser/actions/thinkingTime.js +96 -45
  42. package/dist/src/browser/chromeLifecycle.js +8 -37
  43. package/dist/src/browser/index.js +23 -8
  44. package/dist/src/browser/modelDisplay.js +67 -0
  45. package/dist/src/browser/reattach.js +14 -1
  46. package/dist/src/browser/recoverConversation.js +2 -1
  47. package/dist/src/browser/sessionRunner.js +9 -10
  48. package/dist/src/browser/wslHost.js +50 -0
  49. package/dist/src/cli/browserConfig.js +31 -11
  50. package/dist/src/cli/detach.js +21 -4
  51. package/dist/src/cli/dryRun.js +13 -2
  52. package/dist/src/cli/engine.js +2 -2
  53. package/dist/src/cli/options.js +5 -1
  54. package/dist/src/cli/sessionDisplay.js +60 -8
  55. package/dist/src/cli/sessionLifecycle.js +2 -1
  56. package/dist/src/cli/sessionRunner.js +110 -60
  57. package/dist/src/cli/sessionTable.js +5 -1
  58. package/dist/src/cli/tui/index.js +12 -4
  59. package/dist/src/duration.js +3 -0
  60. package/dist/src/gemini-web/client.js +19 -1
  61. package/dist/src/oracle/modelResolver.js +8 -1
  62. package/dist/src/oracle/request.js +9 -2
  63. package/dist/src/oracle/run.js +43 -3
  64. package/dist/src/oracle/thinkingTime.js +9 -3
  65. package/dist/src/sessionManager.js +41 -12
  66. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  68. package/package.json +17 -17
  69. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -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)
@@ -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
@@ -133,7 +133,7 @@ export function parseThinkingTimeOption(value) {
133
133
  if (normalized) {
134
134
  return normalized;
135
135
  }
136
- throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "extra-high".');
136
+ throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "extra-high", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "xhigh".');
137
137
  }
138
138
  export function normalizeModelOption(value) {
139
139
  return (value ?? "").trim();
@@ -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
  }
@@ -78,6 +78,15 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
78
78
  cwd,
79
79
  log,
80
80
  }, runnerDeps);
81
+ await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
82
+ await sendSessionNotification({
83
+ sessionId: sessionMeta.id,
84
+ sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
85
+ mode,
86
+ model: sessionMeta.model,
87
+ usage: result.usage,
88
+ characters: result.answerText?.length,
89
+ }, notificationSettings, log, result.answerText?.slice(0, 140));
81
90
  if (modelForStatus) {
82
91
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
83
92
  status: "completed",
@@ -103,15 +112,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
103
112
  transport: undefined,
104
113
  error: undefined,
105
114
  });
106
- await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
107
- await sendSessionNotification({
108
- sessionId: sessionMeta.id,
109
- sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
110
- mode,
111
- model: sessionMeta.model,
112
- usage: result.usage,
113
- characters: result.answerText?.length,
114
- }, notificationSettings, log, result.answerText?.slice(0, 140));
115
115
  return;
116
116
  }
117
117
  const multiModels = Array.isArray(runOptions.models) ? runOptions.models.filter(Boolean) : [];
@@ -362,16 +362,6 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
362
362
  if (result.mode !== "live") {
363
363
  throw new Error("Unexpected preview result while running a session.");
364
364
  }
365
- await sessionStore.updateSession(sessionMeta.id, {
366
- status: "completed",
367
- completedAt: new Date().toISOString(),
368
- usage: result.usage,
369
- elapsedMs: result.elapsedMs,
370
- errorMessage: undefined,
371
- response: extractResponseMetadata(result.response),
372
- transport: undefined,
373
- error: undefined,
374
- });
375
365
  if (modelForStatus && singleModelOverride == null) {
376
366
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
377
367
  status: "completed",
@@ -389,6 +379,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
389
379
  usage: result.usage,
390
380
  characters: answerText.length,
391
381
  }, notificationSettings, log, answerText.slice(0, 140));
382
+ await sessionStore.updateSession(sessionMeta.id, {
383
+ status: "completed",
384
+ completedAt: new Date().toISOString(),
385
+ usage: result.usage,
386
+ elapsedMs: result.elapsedMs,
387
+ errorMessage: undefined,
388
+ response: extractResponseMetadata(result.response),
389
+ transport: undefined,
390
+ error: undefined,
391
+ });
392
392
  }
393
393
  catch (error) {
394
394
  const message = formatError(error);
@@ -510,16 +510,60 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
510
510
  const runtime = userError.details
511
511
  ?.runtime;
512
512
  log(dim("Assistant response timed out; marking capture incomplete for reattach."));
513
+ const timeoutResponse = {
514
+ status: "incomplete",
515
+ incompleteReason: "incomplete-capture",
516
+ };
517
+ const timeoutError = {
518
+ category: userError.category,
519
+ message: userError.message,
520
+ details: userError.details,
521
+ };
522
+ const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
523
+ const autoRuntime = runtime ?? currentBrowser?.runtime;
524
+ const willAutoReattach = autoReattachIntervalMs > 0 && Boolean(autoRuntime);
525
+ if (willAutoReattach) {
526
+ if (modelForStatus) {
527
+ await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
528
+ status: "running",
529
+ completedAt: undefined,
530
+ response: timeoutResponse,
531
+ error: timeoutError,
532
+ });
533
+ }
534
+ await sessionStore.updateSession(sessionMeta.id, {
535
+ status: "running",
536
+ completedAt: undefined,
537
+ errorMessage: message,
538
+ mode,
539
+ browser: {
540
+ ...currentBrowser,
541
+ config: browserConfig,
542
+ runtime: autoRuntime,
543
+ },
544
+ response: timeoutResponse,
545
+ error: timeoutError,
546
+ });
547
+ const success = await autoReattachUntilComplete({
548
+ sessionMeta,
549
+ runtime: autoRuntime,
550
+ browserConfig,
551
+ browserMetadata: currentBrowser,
552
+ runOptions,
553
+ modelForStatus,
554
+ notificationSettings,
555
+ log,
556
+ });
557
+ if (success) {
558
+ return;
559
+ }
560
+ }
513
561
  if (modelForStatus) {
514
562
  await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
515
563
  status: "error",
516
564
  completedAt: new Date().toISOString(),
517
- response: { status: "incomplete", incompleteReason: "incomplete-capture" },
518
- error: {
519
- category: userError.category,
520
- message: userError.message,
521
- details: userError.details,
522
- },
565
+ response: timeoutResponse,
566
+ error: timeoutError,
523
567
  });
524
568
  }
525
569
  await sessionStore.updateSession(sessionMeta.id, {
@@ -532,30 +576,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
532
576
  config: browserConfig,
533
577
  runtime: runtime ?? currentBrowser?.runtime,
534
578
  },
535
- response: { status: "incomplete", incompleteReason: "incomplete-capture" },
536
- error: {
537
- category: userError.category,
538
- message: userError.message,
539
- details: userError.details,
540
- },
579
+ response: timeoutResponse,
580
+ error: timeoutError,
541
581
  });
542
- const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
543
- if (autoReattachIntervalMs > 0) {
544
- const autoRuntime = runtime ?? currentBrowser?.runtime;
545
- const success = await autoReattachUntilComplete({
546
- sessionMeta,
547
- runtime: autoRuntime ?? undefined,
548
- browserConfig,
549
- browserMetadata: currentBrowser,
550
- runOptions,
551
- modelForStatus,
552
- notificationSettings,
553
- log,
554
- });
555
- if (success) {
556
- return;
557
- }
558
- }
559
582
  logBrowserReattachGuidance(runtime ?? currentBrowser?.runtime);
560
583
  return;
561
584
  }
@@ -911,6 +934,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
911
934
  }
912
935
  attempt += 1;
913
936
  log(dim(`Auto-reattach attempt ${attempt}...`));
937
+ let captureSucceeded = false;
914
938
  try {
915
939
  const reattachConfig = {
916
940
  ...browserConfig,
@@ -919,6 +943,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
919
943
  const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
920
944
  promptPreview: sessionMeta.promptPreview,
921
945
  });
946
+ captureSucceeded = true;
922
947
  const answerText = result.answerMarkdown || result.answerText || "";
923
948
  const outputTokens = estimateTokenCount(answerText);
924
949
  const artifacts = await ensureSessionArtifacts({
@@ -947,6 +972,18 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
947
972
  },
948
973
  });
949
974
  }
975
+ await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
976
+ await sendSessionNotification({
977
+ sessionId: sessionMeta.id,
978
+ sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
979
+ mode: sessionMeta.mode ?? "browser",
980
+ model: sessionMeta.model ?? runOptions.model,
981
+ usage: {
982
+ inputTokens: 0,
983
+ outputTokens,
984
+ },
985
+ characters: answerText.length,
986
+ }, notificationSettings, log, answerText.slice(0, 140));
950
987
  await sessionStore.updateSession(sessionMeta.id, {
951
988
  status: "completed",
952
989
  completedAt: new Date().toISOString(),
@@ -967,22 +1004,35 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
967
1004
  error: undefined,
968
1005
  transport: undefined,
969
1006
  });
970
- await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
971
- await sendSessionNotification({
972
- sessionId: sessionMeta.id,
973
- sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
974
- mode: sessionMeta.mode ?? "browser",
975
- model: sessionMeta.model ?? runOptions.model,
976
- usage: {
977
- inputTokens: 0,
978
- outputTokens,
979
- },
980
- characters: answerText.length,
981
- }, notificationSettings, log, answerText.slice(0, 140));
982
1007
  log(kleur.green("Auto-reattach succeeded; session marked completed."));
983
1008
  return true;
984
1009
  }
985
1010
  catch (error) {
1011
+ if (captureSucceeded) {
1012
+ const message = formatError(error);
1013
+ if (modelForStatus) {
1014
+ await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
1015
+ status: "error",
1016
+ completedAt: new Date().toISOString(),
1017
+ });
1018
+ }
1019
+ await sessionStore.updateSession(sessionMeta.id, {
1020
+ status: "error",
1021
+ completedAt: new Date().toISOString(),
1022
+ errorMessage: message,
1023
+ browser: {
1024
+ ...browserMetadata,
1025
+ config: browserConfig,
1026
+ runtime,
1027
+ },
1028
+ response: { status: "error", incompleteReason: "incomplete-capture" },
1029
+ error: {
1030
+ category: "internal",
1031
+ message,
1032
+ },
1033
+ });
1034
+ throw error;
1035
+ }
986
1036
  const message = error instanceof Error ? error.message : String(error);
987
1037
  log(dim(`Auto-reattach attempt ${attempt} failed: ${message}`));
988
1038
  }
@@ -3,6 +3,7 @@ import kleur from "kleur";
3
3
  import { MODEL_CONFIGS } from "../oracle.js";
4
4
  import { estimateUsdCost } from "tokentally";
5
5
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
6
+ import { resolveSessionBrowserModelDisplayName } from "../browser/modelDisplay.js";
6
7
  const isRich = (rich) => rich ?? Boolean(process.stdout.isTTY && chalk.level > 0);
7
8
  const dim = (text, rich) => (rich ? kleur.dim(text) : text);
8
9
  export const STATUS_PAD = 9;
@@ -18,7 +19,10 @@ export function formatSessionTableHeader(rich) {
18
19
  export function formatSessionTableRow(meta, options) {
19
20
  const rich = isRich(options?.rich);
20
21
  const status = colorStatus(meta.status ?? "unknown", rich);
21
- const modelLabel = (meta.model ?? "n/a").padEnd(MODEL_PAD);
22
+ const displayModel = (meta.mode ?? meta.options?.mode) === "browser"
23
+ ? resolveSessionBrowserModelDisplayName(meta)
24
+ : (meta.model ?? "n/a");
25
+ const modelLabel = displayModel.padEnd(MODEL_PAD);
22
26
  const model = rich ? chalk.white(modelLabel) : modelLabel;
23
27
  const modeLabel = formatSessionExecutionLabel(meta).padEnd(MODE_PAD);
24
28
  const mode = rich ? chalk.gray(modeLabel) : modeLabel;
@@ -15,6 +15,7 @@ import { resolveNotificationSettings } from "../notifier.js";
15
15
  import { loadUserConfig } from "../../config.js";
16
16
  import { resolveConfiguredMaxFileSizeBytes } from "../fileSize.js";
17
17
  import { formatTokenCount } from "../../oracle/runUtils.js";
18
+ import { formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../../browser/modelDisplay.js";
18
19
  const isTty = () => Boolean(process.stdout.isTTY && chalk.level > 0);
19
20
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
20
21
  const RECENT_WINDOW_HOURS = 24;
@@ -157,7 +158,7 @@ async function showSessionDetail(sessionId) {
157
158
  console.clear();
158
159
  printSessionHeader(meta);
159
160
  if (meta.models && meta.models.length > 0) {
160
- printModelSummaries(meta.models);
161
+ printModelSummaries(meta);
161
162
  }
162
163
  const prompt = await readStoredPrompt(sessionId);
163
164
  if (prompt) {
@@ -250,7 +251,10 @@ function printSessionHeader(meta) {
250
251
  console.log(`${chalk.white("Status:")} ${meta.status}`);
251
252
  console.log(`${chalk.white("Created:")} ${meta.createdAt}`);
252
253
  if (meta.model) {
253
- console.log(`${chalk.white("Model:")} ${meta.model}`);
254
+ const modelLabel = (meta.mode ?? meta.options?.mode) === "browser"
255
+ ? resolveSessionBrowserModelDisplayName(meta)
256
+ : meta.model;
257
+ console.log(`${chalk.white("Model:")} ${modelLabel}`);
254
258
  }
255
259
  const mode = meta.mode ?? meta.options?.mode;
256
260
  if (mode) {
@@ -260,7 +264,8 @@ function printSessionHeader(meta) {
260
264
  console.log(chalk.red(`Error: ${meta.errorMessage}`));
261
265
  }
262
266
  }
263
- function printModelSummaries(models) {
267
+ function printModelSummaries(meta) {
268
+ const models = meta.models ?? [];
264
269
  if (models.length === 0) {
265
270
  return;
266
271
  }
@@ -269,7 +274,10 @@ function printModelSummaries(models) {
269
274
  const usage = run.usage
270
275
  ? ` tok=${formatTokenCount(run.usage.outputTokens ?? 0)}/${formatTokenCount(run.usage.totalTokens ?? 0)}`
271
276
  : "";
272
- console.log(` - ${chalk.cyan(run.model)} ${run.status}${usage}`);
277
+ const modelLabel = (meta.mode ?? meta.options?.mode) === "browser"
278
+ ? formatSessionBrowserModelWithRequestedKey(meta, run.model)
279
+ : run.model;
280
+ console.log(` - ${chalk.cyan(modelLabel)} — ${run.status}${usage}`);
273
281
  }
274
282
  console.log("");
275
283
  }