@steipete/oracle 0.11.1 → 0.12.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 (49) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/modelSelection.js +74 -20
  4. package/dist/src/browser/actions/navigation.js +5 -3
  5. package/dist/src/browser/actions/promptComposer.js +76 -18
  6. package/dist/src/browser/actions/thinkingTime.js +133 -19
  7. package/dist/src/browser/constants.js +1 -1
  8. package/dist/src/browser/index.js +78 -9
  9. package/dist/src/browser/manualLoginProfile.js +54 -0
  10. package/dist/src/browser/projectSourcesRunner.js +16 -5
  11. package/dist/src/browser/prompt.js +56 -37
  12. package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
  13. package/dist/src/browser/reattachability.js +22 -0
  14. package/dist/src/browser/sessionRunner.js +73 -1
  15. package/dist/src/browser/utils.js +1 -47
  16. package/dist/src/browser/zipBundle.js +152 -0
  17. package/dist/src/cli/browserConfig.js +13 -11
  18. package/dist/src/cli/browserDefaults.js +2 -1
  19. package/dist/src/cli/docsCheck.js +186 -0
  20. package/dist/src/cli/engine.js +11 -4
  21. package/dist/src/cli/options.js +12 -6
  22. package/dist/src/cli/perfTrace.js +242 -0
  23. package/dist/src/cli/promptRequirement.js +2 -0
  24. package/dist/src/cli/providerDoctor.js +85 -0
  25. package/dist/src/cli/runOptions.js +46 -16
  26. package/dist/src/cli/sessionDisplay.js +47 -4
  27. package/dist/src/cli/sessionLifecycle.js +38 -0
  28. package/dist/src/cli/sessionRunner.js +272 -3
  29. package/dist/src/cli/sessionTable.js +2 -1
  30. package/dist/src/duration.js +47 -0
  31. package/dist/src/mcp/tools/consult.js +19 -3
  32. package/dist/src/mcp/types.js +1 -0
  33. package/dist/src/mcp/utils.js +4 -1
  34. package/dist/src/oracle/baseUrl.js +17 -0
  35. package/dist/src/oracle/client.js +1 -22
  36. package/dist/src/oracle/config.js +17 -4
  37. package/dist/src/oracle/gemini.js +2 -22
  38. package/dist/src/oracle/geminiModels.js +21 -0
  39. package/dist/src/oracle/modelResolver.js +7 -1
  40. package/dist/src/oracle/multiModelRunner.js +20 -2
  41. package/dist/src/oracle/providerFailures.js +204 -0
  42. package/dist/src/oracle/providerRoutePlan.js +308 -0
  43. package/dist/src/oracle/providerRouting.js +92 -0
  44. package/dist/src/oracle/run.js +104 -107
  45. package/dist/src/oracle.js +1 -0
  46. package/dist/src/remote/client.js +8 -0
  47. package/dist/src/remote/server.js +26 -0
  48. package/dist/src/sessionManager.js +43 -23
  49. package/package.json +15 -12
@@ -0,0 +1,242 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ const SECRET_VALUE_FLAGS = new Set([
5
+ "--api-key",
6
+ "--browser-follow-up",
7
+ "--browser-inline-cookies",
8
+ "--browser-inline-cookies-file",
9
+ "--message",
10
+ "--prompt",
11
+ "--remote-token",
12
+ "--token",
13
+ "-p",
14
+ ]);
15
+ const VALUE_FLAGS = new Set([
16
+ "--aspect",
17
+ "--azure-api-version",
18
+ "--azure-deployment",
19
+ "--azure-endpoint",
20
+ "--base-url",
21
+ "--browser-archive",
22
+ "--browser-attachments",
23
+ "--browser-auto-reattach-delay",
24
+ "--browser-auto-reattach-interval",
25
+ "--browser-auto-reattach-timeout",
26
+ "--browser-bundle-format",
27
+ "--browser-cookie-names",
28
+ "--browser-cookie-path",
29
+ "--browser-cookie-wait",
30
+ "--browser-input-timeout",
31
+ "--browser-max-concurrent-tabs",
32
+ "--browser-model-strategy",
33
+ "--browser-port",
34
+ "--browser-profile-lock-timeout",
35
+ "--browser-recheck-delay",
36
+ "--browser-recheck-timeout",
37
+ "--browser-research",
38
+ "--browser-reuse-wait",
39
+ "--browser-tab",
40
+ "--browser-timeout",
41
+ "--browser-url",
42
+ "--chatgpt-url",
43
+ "--engine",
44
+ "--followup",
45
+ "--followup-model",
46
+ "--heartbeat",
47
+ "--http-timeout",
48
+ "--max-file-size-bytes",
49
+ "--model",
50
+ "--models",
51
+ "--output",
52
+ "--partial",
53
+ "--perf-trace-path",
54
+ "--provider",
55
+ "--remote-chrome",
56
+ "--remote-host",
57
+ "--slug",
58
+ "--timeout",
59
+ "--write-output",
60
+ "--youtube",
61
+ "--zombie-timeout",
62
+ "-e",
63
+ "-m",
64
+ "-s",
65
+ ]);
66
+ export function isTraceValueFlag(flag) {
67
+ return SECRET_VALUE_FLAGS.has(flag) || VALUE_FLAGS.has(flag);
68
+ }
69
+ class DisabledPerfTrace {
70
+ mark() { }
71
+ wrapFirstOutput() { }
72
+ flush() { }
73
+ }
74
+ class FilePerfTrace {
75
+ outputPath;
76
+ options;
77
+ events = [];
78
+ wrapped = false;
79
+ firstOutput = false;
80
+ flushed = false;
81
+ constructor(outputPath, options) {
82
+ this.outputPath = outputPath;
83
+ this.options = options;
84
+ }
85
+ mark(name, data) {
86
+ this.events.push({
87
+ name,
88
+ ms: Number(performance.now().toFixed(3)),
89
+ data,
90
+ });
91
+ }
92
+ wrapFirstOutput() {
93
+ if (this.wrapped)
94
+ return;
95
+ this.wrapped = true;
96
+ const wrap = (stream) => {
97
+ const original = stream.write.bind(stream);
98
+ stream.write = ((...args) => {
99
+ if (!this.firstOutput) {
100
+ this.firstOutput = true;
101
+ this.mark("first-output", {
102
+ stream: stream === process.stderr ? "stderr" : "stdout",
103
+ });
104
+ }
105
+ return original(...args);
106
+ });
107
+ };
108
+ wrap(process.stdout);
109
+ wrap(process.stderr);
110
+ }
111
+ flush(exitCode) {
112
+ if (this.flushed)
113
+ return;
114
+ this.flushed = true;
115
+ this.mark("exit", { exitCode: exitCode ?? 0 });
116
+ const payload = {
117
+ version: this.options.version,
118
+ argv: sanitizeTraceArgv(this.options.argv),
119
+ cwd: this.options.cwd ?? process.cwd(),
120
+ pid: process.pid,
121
+ node: process.version,
122
+ timeOrigin: performance.timeOrigin,
123
+ totalMs: Number(performance.now().toFixed(3)),
124
+ events: this.events,
125
+ };
126
+ writeFileSync(this.outputPath, `${JSON.stringify(payload, null, 2)}\n`);
127
+ }
128
+ }
129
+ export function createPerfTrace(options) {
130
+ const envValue = process.env.ORACLE_PERF_TRACE;
131
+ const optionValue = options.value;
132
+ if (!optionValue && !envValue) {
133
+ return new DisabledPerfTrace();
134
+ }
135
+ const rawValue = typeof optionValue === "string" ? optionValue : envValue;
136
+ const outputPath = rawValue && rawValue !== "1" && rawValue !== "true"
137
+ ? path.resolve(options.cwd ?? process.cwd(), rawValue)
138
+ : path.join(options.cwd ?? process.cwd(), `.oracle-perf-${new Date().toISOString().replace(/[:.]/g, "-")}-${process.pid}.json`);
139
+ const trace = new FilePerfTrace(outputPath, options);
140
+ trace.wrapFirstOutput();
141
+ trace.mark("cli-module-ready");
142
+ return trace;
143
+ }
144
+ export function deriveDetachedPerfTraceEnv(value, sessionId) {
145
+ const trimmed = value?.trim();
146
+ if (!trimmed || trimmed === "1" || trimmed.toLowerCase() === "true")
147
+ return value;
148
+ const safeSessionId = sessionId.replace(/[^A-Za-z0-9._-]/g, "_");
149
+ const lastSlash = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
150
+ const lastDot = trimmed.lastIndexOf(".");
151
+ if (lastDot > lastSlash) {
152
+ return `${trimmed.slice(0, lastDot)}.${safeSessionId}${trimmed.slice(lastDot)}`;
153
+ }
154
+ return `${trimmed}.${safeSessionId}.json`;
155
+ }
156
+ export function resolveDetachedPerfTraceEnv(cliValue, envValue, sessionId) {
157
+ if (typeof cliValue === "string") {
158
+ return deriveDetachedPerfTraceEnv(cliValue, sessionId);
159
+ }
160
+ if (cliValue === true) {
161
+ return "1";
162
+ }
163
+ return deriveDetachedPerfTraceEnv(envValue, sessionId);
164
+ }
165
+ export function buildDetachedPerfTraceEnv(env, cliValue, sessionId) {
166
+ const nextEnv = { ...env };
167
+ const traceValue = resolveDetachedPerfTraceEnv(cliValue, env.ORACLE_PERF_TRACE, sessionId);
168
+ if (traceValue) {
169
+ nextEnv.ORACLE_PERF_TRACE = traceValue;
170
+ }
171
+ else {
172
+ delete nextEnv.ORACLE_PERF_TRACE;
173
+ }
174
+ return nextEnv;
175
+ }
176
+ export function sanitizeTraceArgv(argv) {
177
+ const sanitized = [];
178
+ let redactNext = false;
179
+ let valueNext = false;
180
+ let afterDoubleDash = false;
181
+ for (const arg of argv) {
182
+ if (afterDoubleDash) {
183
+ sanitized.push("[redacted-positional]");
184
+ continue;
185
+ }
186
+ if (arg === "--") {
187
+ sanitized.push(arg);
188
+ afterDoubleDash = true;
189
+ continue;
190
+ }
191
+ if (redactNext) {
192
+ sanitized.push("[redacted]");
193
+ redactNext = false;
194
+ continue;
195
+ }
196
+ if (valueNext) {
197
+ sanitized.push(redactPotentialSecret(arg));
198
+ valueNext = false;
199
+ continue;
200
+ }
201
+ const equalsIndex = arg.indexOf("=");
202
+ const flag = equalsIndex >= 0 ? arg.slice(0, equalsIndex) : arg;
203
+ if (equalsIndex >= 0 && SECRET_VALUE_FLAGS.has(flag)) {
204
+ sanitized.push(`${flag}=[redacted]`);
205
+ continue;
206
+ }
207
+ if (arg.startsWith("-p") && arg.length > 2) {
208
+ sanitized.push("-p[redacted]");
209
+ continue;
210
+ }
211
+ if (equalsIndex >= 0) {
212
+ sanitized.push(`${flag}=${redactPotentialSecret(arg.slice(equalsIndex + 1))}`);
213
+ continue;
214
+ }
215
+ if (SECRET_VALUE_FLAGS.has(arg)) {
216
+ sanitized.push(arg);
217
+ redactNext = true;
218
+ continue;
219
+ }
220
+ if (VALUE_FLAGS.has(arg)) {
221
+ sanitized.push(arg);
222
+ valueNext = true;
223
+ continue;
224
+ }
225
+ if (!arg.startsWith("-")) {
226
+ sanitized.push("[redacted-positional]");
227
+ continue;
228
+ }
229
+ sanitized.push(arg);
230
+ }
231
+ return sanitized;
232
+ }
233
+ function redactPotentialSecret(value) {
234
+ return value
235
+ .replace(/\bBearer\s+[A-Za-z0-9._\-+/=]+/gi, "Bearer [redacted]")
236
+ .replace(/:\/\/([^:/?#\s]+):([^@/?#\s]+)@/g, "://$1:[redacted]@")
237
+ .replace(/([?&](?:access_)?token=)[^&#\s]+/gi, "$1[redacted]")
238
+ .replace(/([?&](?:api[-_]?key|auth|authorization|password|secret)=)[^&#\s]+/gi, "$1[redacted]")
239
+ .replace(/\bsk-(?:ant-|or-)?[A-Za-z0-9_-]{8,}\b/g, "sk-...[redacted]")
240
+ .replace(/\bxai-[A-Za-z0-9_-]{8,}\b/g, "xai-...[redacted]")
241
+ .replace(/\bAIza[0-9A-Za-z_-]{8,}\b/g, "AIza...[redacted]");
242
+ }
@@ -10,6 +10,8 @@ export function shouldRequirePrompt(rawArgs, options) {
10
10
  options.execSession ||
11
11
  options.status ||
12
12
  options.debugHelp ||
13
+ options.route ||
14
+ options.preflight ||
13
15
  firstArg === "status" ||
14
16
  firstArg === "session");
15
17
  const requiresPrompt = options.renderMarkdown || Boolean(options.preview) || Boolean(options.dryRun) || !bypassPrompt;
@@ -0,0 +1,85 @@
1
+ import chalk from "chalk";
2
+ import { DEFAULT_MODEL } from "../oracle/config.js";
3
+ import { resolveApiModel } from "./options.js";
4
+ import { loadUserConfig } from "../config.js";
5
+ import { buildProviderRoutePlan } from "../oracle/providerRoutePlan.js";
6
+ export async function runProviderDoctor(options) {
7
+ if (!options.providers) {
8
+ console.log("Run `oracle doctor --providers` to inspect API provider readiness.");
9
+ return;
10
+ }
11
+ const { config: userConfig } = await loadUserConfig();
12
+ const providerMode = resolveProviderMode(options);
13
+ const azure = resolveAzureOptions(options, userConfig);
14
+ const models = resolveModels(options, userConfig);
15
+ const plans = models.map((model) => buildProviderRoutePlan({
16
+ model,
17
+ providerMode,
18
+ azure,
19
+ baseUrl: options.baseUrl ?? userConfig.apiBaseUrl,
20
+ env: process.env,
21
+ }));
22
+ if (options.json) {
23
+ console.log(JSON.stringify({ providers: plans }, null, 2));
24
+ process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
25
+ return;
26
+ }
27
+ printProviderPlans(plans);
28
+ process.exitCode = plans.some((plan) => !plan.ok) ? 1 : 0;
29
+ }
30
+ export function printProviderPlans(plans, { title = "Provider readiness" } = {}) {
31
+ console.log(chalk.bold(title));
32
+ console.log("");
33
+ for (const plan of plans) {
34
+ const status = plan.ok ? chalk.green("ok") : chalk.red("not ready");
35
+ console.log(`${plan.model}: ${status}`);
36
+ console.log(chalk.dim(` provider: ${plan.providerLabel}`));
37
+ console.log(chalk.dim(` base: ${plan.base || "(none)"}`));
38
+ console.log(chalk.dim(` key: ${plan.keyPreview}`));
39
+ if (plan.isAzureOpenAI || plan.azureDeploymentName) {
40
+ console.log(chalk.dim(` azure deployment: ${plan.azureDeploymentName ?? "none"}`));
41
+ }
42
+ if (plan.azureNote) {
43
+ console.log(chalk.dim(` azure: ${plan.azureNote}`));
44
+ }
45
+ if (plan.error) {
46
+ console.log(chalk.dim(` error: ${plan.error}`));
47
+ }
48
+ console.log("");
49
+ }
50
+ }
51
+ function resolveModels(options, userConfig) {
52
+ const entries = Array.isArray(options.models) && options.models.length > 0
53
+ ? options.models
54
+ : typeof options.models === "string" && options.models.trim().length > 0
55
+ ? options.models
56
+ .split(",")
57
+ .map((entry) => entry.trim())
58
+ .filter(Boolean)
59
+ : [options.model ?? userConfig.model ?? DEFAULT_MODEL];
60
+ return Array.from(new Set(entries.map((entry) => resolveApiModel(entry))));
61
+ }
62
+ function resolveProviderMode(options) {
63
+ const provider = options.provider ?? "auto";
64
+ if (provider === "azure" && options.azure === false) {
65
+ throw new Error("--provider azure cannot be combined with --no-azure.");
66
+ }
67
+ if (options.azure === false) {
68
+ return "openai";
69
+ }
70
+ return provider;
71
+ }
72
+ function resolveAzureOptions(options, userConfig) {
73
+ const endpoint = firstNonEmpty(options.azureEndpoint, process.env.AZURE_OPENAI_ENDPOINT, userConfig.azure?.endpoint);
74
+ if (!endpoint) {
75
+ return undefined;
76
+ }
77
+ return {
78
+ endpoint,
79
+ deployment: firstNonEmpty(options.azureDeployment, process.env.AZURE_OPENAI_DEPLOYMENT, userConfig.azure?.deployment),
80
+ apiVersion: firstNonEmpty(options.azureApiVersion, process.env.AZURE_OPENAI_API_VERSION, userConfig.azure?.apiVersion),
81
+ };
82
+ }
83
+ function firstNonEmpty(...values) {
84
+ return values.find((value) => value?.trim());
85
+ }
@@ -5,41 +5,59 @@ import { resolveGeminiModelId } from "../oracle/gemini.js";
5
5
  import { PromptValidationError } from "../oracle/errors.js";
6
6
  import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
7
7
  import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
8
+ import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
8
9
  export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
9
- const resolvedEngine = resolveEngineWithConfig({ engine, configEngine: userConfig?.engine, env });
10
+ const resolvedEngine = resolveEngineWithConfig({
11
+ engine,
12
+ configEngine: userConfig?.engine,
13
+ env,
14
+ });
10
15
  const browserRequested = engine === "browser";
11
16
  const browserConfigured = userConfig?.engine === "browser";
17
+ const envBrowserConfigured = (env.ORACLE_ENGINE ?? "").trim().toLowerCase() === "browser";
12
18
  const requestedModelList = Array.isArray(models) ? models : [];
13
19
  const normalizedRequestedModels = requestedModelList
14
20
  .map((entry) => normalizeModelOption(entry))
15
21
  .filter(Boolean);
16
22
  const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
17
- const inferredModel = resolvedEngine === "browser" && normalizedRequestedModels.length === 0
18
- ? inferModelFromLabel(cliModelArg)
19
- : resolveApiModel(cliModelArg);
20
- // Browser engine maps Pro/legacy aliases to the latest ChatGPT picker targets.
21
- const resolvedModel = resolvedEngine === "browser" ? normalizeChatGptModelForBrowser(inferredModel) : inferredModel;
22
- const isCodex = resolvedModel.startsWith("gpt-5.1-codex");
23
- const isClaude = resolvedModel.startsWith("claude");
24
- const isGrok = resolvedModel.startsWith("grok");
25
- const isGeminiApiOnly = resolvedModel === "gemini-3.1-pro";
23
+ const apiModel = resolveApiModel(cliModelArg);
24
+ const browserModel = normalizeChatGptModelForBrowser(inferModelFromLabel(cliModelArg));
25
+ const isCodex = apiModel.startsWith("gpt-5.1-codex");
26
+ const isClaude = apiModel.startsWith("claude");
27
+ const isGrok = apiModel.startsWith("grok");
28
+ const isGeminiApiOnly = apiModel === "gemini-3.1-pro";
26
29
  const engineWasBrowser = resolvedEngine === "browser";
27
30
  const allModels = normalizedRequestedModels.length > 0
28
31
  ? Array.from(new Set(normalizedRequestedModels.map((entry) => resolveApiModel(entry))))
29
- : [resolvedModel];
32
+ : [apiModel];
33
+ const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
30
34
  const includesGeminiApiOnly = allModels.some((m) => m === "gemini-3.1-pro");
31
35
  if ((browserRequested || browserConfigured) && includesGeminiApiOnly) {
32
36
  throw new PromptValidationError("gemini-3.1-pro is API-only today. Use --engine api or switch to gemini-3-pro for Gemini web.", { engine: "browser", models: allModels });
33
37
  }
34
38
  const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
35
- const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) && allModels.some((m) => !isBrowserCompatible(m));
39
+ const hasNonBrowserCompatibleTarget = (browserRequested || browserConfigured) &&
40
+ browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
36
41
  if (hasNonBrowserCompatibleTarget) {
37
42
  throw new PromptValidationError("Browser engine only supports GPT and Gemini models. Re-run with --engine api for Grok, Claude, or other models.", { engine: "browser", models: allModels });
38
43
  }
39
- const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || isGeminiApiOnly);
40
- const fixedEngine = isCodex || isClaude || isGrok || isGeminiApiOnly || normalizedRequestedModels.length > 0
44
+ const azure = resolveAzureOptions(userConfig, env);
45
+ const azureAutoApi = Boolean(azure?.endpoint) &&
46
+ !browserRequested &&
47
+ !browserConfigured &&
48
+ !envBrowserConfigured &&
49
+ allModels.some(isAzureOpenAICandidateModel);
50
+ const engineCoercedToApi = engineWasBrowser && (isCodex || isClaude || isGrok || isGeminiApiOnly || azureAutoApi);
51
+ const fixedEngine = isCodex ||
52
+ isClaude ||
53
+ isGrok ||
54
+ isGeminiApiOnly ||
55
+ azureAutoApi ||
56
+ normalizedRequestedModels.length > 0
41
57
  ? "api"
42
58
  : resolvedEngine;
59
+ // Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
60
+ const resolvedModel = fixedEngine === "browser" ? browserModel : apiModel;
43
61
  const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
44
62
  ? `${prompt.trim()}\n${userConfig.promptSuffix}`
45
63
  : prompt;
@@ -66,11 +84,12 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
66
84
  filesReport: userConfig?.filesReport,
67
85
  background: userConfig?.background,
68
86
  baseUrl,
87
+ azure,
69
88
  effectiveModelId,
70
89
  };
71
90
  return { runOptions, resolvedEngine: fixedEngine, engineCoercedToApi };
72
91
  }
73
- function resolveEngineWithConfig({ engine, configEngine, env, }) {
92
+ function resolveEngineWithConfig({ engine, configEngine, apiProviderRequested, env, }) {
74
93
  if (engine)
75
94
  return engine;
76
95
  const envOverride = (env.ORACLE_ENGINE ?? "").trim().toLowerCase();
@@ -79,7 +98,18 @@ function resolveEngineWithConfig({ engine, configEngine, env, }) {
79
98
  }
80
99
  if (configEngine)
81
100
  return configEngine;
82
- return resolveEngine({ engine: undefined, env });
101
+ return resolveEngine({ engine: undefined, apiProviderRequested, env });
102
+ }
103
+ function resolveAzureOptions(userConfig, env) {
104
+ const endpoint = env.AZURE_OPENAI_ENDPOINT ?? userConfig?.azure?.endpoint;
105
+ if (!endpoint?.trim()) {
106
+ return undefined;
107
+ }
108
+ return {
109
+ endpoint,
110
+ deployment: env.AZURE_OPENAI_DEPLOYMENT ?? userConfig?.azure?.deployment,
111
+ apiVersion: env.AZURE_OPENAI_API_VERSION ?? userConfig?.azure?.apiVersion,
112
+ };
83
113
  }
84
114
  function resolveEffectiveModelId(model) {
85
115
  if (typeof model === "string" && model.startsWith("gemini")) {
@@ -6,10 +6,12 @@ import { formatFinishLine } from "../oracle/finishLine.js";
6
6
  import { sessionStore, wait } from "../sessionStore.js";
7
7
  import { formatTokenCount, formatTokenValue } from "../oracle/runUtils.js";
8
8
  import { resumeBrowserSession } from "../browser/reattach.js";
9
+ import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
9
10
  import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
10
11
  import { estimateTokenCount } from "../browser/utils.js";
11
12
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
12
13
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
14
+ import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
13
15
  const isTty = () => Boolean(process.stdout.isTTY);
14
16
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
15
17
  export const MAX_RENDER_BYTES = 200_000;
@@ -173,9 +175,16 @@ export async function attachSession(sessionId, options) {
173
175
  hasFallbackSessionInfo &&
174
176
  isDeepResearchPlaceholderCapture(metadata, await sessionStore.readLog(sessionId).catch(() => ""));
175
177
  const completedDeepResearchPlaceholder = metadata.status === "completed" && deepResearchPlaceholderCapture;
178
+ const hasRecoverableConversation = hasRecoverableChatGptConversation(runtime);
179
+ const hasLiveChromeFallback = Boolean((metadata.status === "running" || hasIncompleteCapture || completedDeepResearchPlaceholder) &&
180
+ (runtime?.chromePort || runtime?.chromeBrowserWSEndpoint || runtime?.chromeProfileRoot));
176
181
  const canReattach = (statusAllowsReattach || completedDeepResearchPlaceholder) &&
177
182
  metadata.mode === "browser" &&
178
183
  hasFallbackSessionInfo &&
184
+ (hasRecoverableConversation ||
185
+ runtime?.promptSubmitted ||
186
+ hasLiveChromeFallback ||
187
+ completedDeepResearchPlaceholder) &&
179
188
  (hasChromeDisconnect ||
180
189
  hasIncompleteCapture ||
181
190
  completedDeepResearchPlaceholder ||
@@ -219,6 +228,8 @@ export async function attachSession(sessionId, options) {
219
228
  browser: {
220
229
  config: metadata.browser?.config,
221
230
  runtime,
231
+ modelSelection: metadata.browser?.modelSelection,
232
+ warnings: metadata.browser?.warnings,
222
233
  },
223
234
  artifacts,
224
235
  response: { status: "completed" },
@@ -266,6 +277,11 @@ export async function attachSession(sessionId, options) {
266
277
  }
267
278
  console.log(`Created: ${metadata.createdAt}`);
268
279
  console.log(`Status: ${metadata.status}`);
280
+ if (metadata.lifecycle) {
281
+ const attached = metadata.lifecycle.attached ? "attached" : "detached";
282
+ console.log(`Execution: ${formatSessionExecutionLabel(metadata)} (${attached})`);
283
+ console.log(`Reattach: ${metadata.lifecycle.reattachCommand}`);
284
+ }
269
285
  if (metadata.models && metadata.models.length > 0) {
270
286
  console.log("Models:");
271
287
  for (const run of metadata.models) {
@@ -278,6 +294,13 @@ export async function attachSession(sessionId, options) {
278
294
  else if (metadata.model) {
279
295
  console.log(`Model: ${metadata.model}`);
280
296
  }
297
+ const browserEvidence = formatBrowserEvidence(metadata);
298
+ if (browserEvidence) {
299
+ console.log("Browser evidence:");
300
+ for (const line of browserEvidence) {
301
+ console.log(dim(`- ${line}`));
302
+ }
303
+ }
281
304
  if (metadata.artifacts && metadata.artifacts.length > 0) {
282
305
  console.log("Artifacts:");
283
306
  for (const artifact of metadata.artifacts) {
@@ -299,7 +322,7 @@ export async function attachSession(sessionId, options) {
299
322
  console.log(dim(`User error: ${userErrorSummary}`));
300
323
  }
301
324
  }
302
- const shouldTrimIntro = initialStatus === "completed" || initialStatus === "error";
325
+ const shouldTrimIntro = initialStatus === "completed" || initialStatus === "partial" || initialStatus === "error";
303
326
  if (options?.renderPrompt !== false) {
304
327
  const prompt = await readStoredPrompt(sessionId);
305
328
  if (prompt) {
@@ -422,7 +445,7 @@ export async function attachSession(sessionId, options) {
422
445
  if (!latest) {
423
446
  break;
424
447
  }
425
- if (latest.status === "completed" || latest.status === "error") {
448
+ if (latest.status === "completed" || latest.status === "partial" || latest.status === "error") {
426
449
  await printNew();
427
450
  flushRemainder();
428
451
  if (!options?.suppressMetadata) {
@@ -430,10 +453,11 @@ export async function attachSession(sessionId, options) {
430
453
  console.log("\nResult:");
431
454
  console.log(`Session failed: ${latest.errorMessage}`);
432
455
  }
433
- if (latest.status === "completed" && latest.usage) {
456
+ if ((latest.status === "completed" || latest.status === "partial") && latest.usage) {
434
457
  const summary = formatCompletionSummary(latest, { includeSlug: true });
435
458
  if (summary) {
436
- console.log(`\n${chalk.green.bold(summary)}`);
459
+ const color = latest.status === "partial" ? chalk.yellow.bold : chalk.green.bold;
460
+ console.log(`\n${color(summary)}`);
437
461
  }
438
462
  else {
439
463
  const usage = latest.usage;
@@ -495,6 +519,25 @@ export function formatUserErrorMetadata(metadata) {
495
519
  }
496
520
  return parts.length > 0 ? parts.join(" | ") : null;
497
521
  }
522
+ export function formatBrowserEvidence(metadata) {
523
+ const browser = metadata.browser;
524
+ if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
525
+ return null;
526
+ }
527
+ const lines = [];
528
+ const evidence = browser.modelSelection;
529
+ if (evidence) {
530
+ const requested = evidence.requestedModel ?? "(none)";
531
+ const resolved = evidence.resolvedLabel ?? "(unavailable)";
532
+ const strategy = evidence.strategy ?? "(default)";
533
+ const verified = evidence.verified ? "yes" : "no";
534
+ lines.push(`model requested=${requested}; resolved=${resolved}; status=${evidence.status}; strategy=${strategy}; verified=${verified}`);
535
+ }
536
+ for (const warning of browser.warnings ?? []) {
537
+ lines.push(`warning ${warning.code}: ${warning.message}`);
538
+ }
539
+ return lines.length > 0 ? lines : null;
540
+ }
498
541
  export function buildReattachLine(metadata) {
499
542
  if (!metadata.id) {
500
543
  return null;
@@ -0,0 +1,38 @@
1
+ export function buildSessionLifecycle({ engine, detached, reattachCommand, }) {
2
+ return {
3
+ engine,
4
+ execution: detached ? "background" : "foreground",
5
+ attached: !detached,
6
+ detached,
7
+ reattachCommand,
8
+ };
9
+ }
10
+ export function formatSessionLifecycleBlock(meta) {
11
+ const lifecycle = meta.lifecycle;
12
+ if (!lifecycle) {
13
+ return [];
14
+ }
15
+ const modelCount = meta.models?.length ?? (meta.model ? 1 : 0);
16
+ const detachValue = lifecycle.detached
17
+ ? lifecycle.execution === "background"
18
+ ? "yes, polling"
19
+ : "yes"
20
+ : "no";
21
+ const lines = [
22
+ `Session: ${meta.id}`,
23
+ `Mode: ${lifecycle.engine} ${lifecycle.execution}`,
24
+ `Models: ${modelCount > 1 ? `${modelCount} parallel` : String(modelCount || 1)}`,
25
+ `Detach: ${detachValue}`,
26
+ `Reattach: ${lifecycle.reattachCommand}`,
27
+ ];
28
+ return lines;
29
+ }
30
+ export function formatSessionExecutionLabel(meta) {
31
+ const lifecycle = meta.lifecycle;
32
+ if (!lifecycle) {
33
+ return meta.mode ?? meta.options?.mode ?? "api";
34
+ }
35
+ const engine = lifecycle.engine === "browser" ? "br" : lifecycle.engine;
36
+ const execution = lifecycle.execution === "background" ? "bg" : "fg";
37
+ return `${engine}/${execution}`;
38
+ }