@indykish/oracle 0.9.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.
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/assets-oracle-icon.png +0 -0
- package/dist/bin/oracle-cli.js +1252 -0
- package/dist/bin/oracle-mcp.js +6 -0
- package/dist/scripts/agent-send.js +147 -0
- package/dist/scripts/browser-tools.js +536 -0
- package/dist/scripts/check.js +21 -0
- package/dist/scripts/debug/extract-chatgpt-response.js +53 -0
- package/dist/scripts/docs-list.js +110 -0
- package/dist/scripts/git-policy.js +125 -0
- package/dist/scripts/run-cli.js +14 -0
- package/dist/scripts/runner.js +1378 -0
- package/dist/scripts/test-browser.js +103 -0
- package/dist/scripts/test-remote-chrome.js +68 -0
- package/dist/src/bridge/connection.js +103 -0
- package/dist/src/bridge/userConfigFile.js +28 -0
- package/dist/src/browser/actions/assistantResponse.js +1067 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +138 -0
- package/dist/src/browser/actions/attachments.js +1910 -0
- package/dist/src/browser/actions/domEvents.js +19 -0
- package/dist/src/browser/actions/modelSelection.js +485 -0
- package/dist/src/browser/actions/navigation.js +445 -0
- package/dist/src/browser/actions/promptComposer.js +485 -0
- package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
- package/dist/src/browser/actions/thinkingTime.js +206 -0
- package/dist/src/browser/chromeLifecycle.js +344 -0
- package/dist/src/browser/config.js +103 -0
- package/dist/src/browser/constants.js +71 -0
- package/dist/src/browser/cookies.js +191 -0
- package/dist/src/browser/detect.js +164 -0
- package/dist/src/browser/domDebug.js +36 -0
- package/dist/src/browser/index.js +1741 -0
- package/dist/src/browser/modelStrategy.js +13 -0
- package/dist/src/browser/pageActions.js +5 -0
- package/dist/src/browser/policies.js +43 -0
- package/dist/src/browser/profileState.js +280 -0
- package/dist/src/browser/prompt.js +152 -0
- package/dist/src/browser/promptSummary.js +20 -0
- package/dist/src/browser/reattach.js +186 -0
- package/dist/src/browser/reattachHelpers.js +382 -0
- package/dist/src/browser/sessionRunner.js +119 -0
- package/dist/src/browser/types.js +1 -0
- package/dist/src/browser/utils.js +122 -0
- package/dist/src/browserMode.js +1 -0
- package/dist/src/cli/bridge/claudeConfig.js +54 -0
- package/dist/src/cli/bridge/client.js +73 -0
- package/dist/src/cli/bridge/codexConfig.js +43 -0
- package/dist/src/cli/bridge/doctor.js +107 -0
- package/dist/src/cli/bridge/host.js +259 -0
- package/dist/src/cli/browserConfig.js +278 -0
- package/dist/src/cli/browserDefaults.js +81 -0
- package/dist/src/cli/bundleWarnings.js +9 -0
- package/dist/src/cli/clipboard.js +10 -0
- package/dist/src/cli/detach.js +11 -0
- package/dist/src/cli/dryRun.js +105 -0
- package/dist/src/cli/duplicatePromptGuard.js +14 -0
- package/dist/src/cli/engine.js +41 -0
- package/dist/src/cli/errorUtils.js +9 -0
- package/dist/src/cli/format.js +13 -0
- package/dist/src/cli/help.js +77 -0
- package/dist/src/cli/hiddenAliases.js +22 -0
- package/dist/src/cli/markdownBundle.js +17 -0
- package/dist/src/cli/markdownRenderer.js +97 -0
- package/dist/src/cli/notifier.js +306 -0
- package/dist/src/cli/options.js +281 -0
- package/dist/src/cli/oscUtils.js +2 -0
- package/dist/src/cli/promptRequirement.js +17 -0
- package/dist/src/cli/renderFlags.js +9 -0
- package/dist/src/cli/renderOutput.js +26 -0
- package/dist/src/cli/rootAlias.js +30 -0
- package/dist/src/cli/runOptions.js +78 -0
- package/dist/src/cli/sessionCommand.js +111 -0
- package/dist/src/cli/sessionDisplay.js +567 -0
- package/dist/src/cli/sessionRunner.js +602 -0
- package/dist/src/cli/sessionTable.js +92 -0
- package/dist/src/cli/tagline.js +258 -0
- package/dist/src/cli/tui/index.js +486 -0
- package/dist/src/cli/writeOutputPath.js +21 -0
- package/dist/src/config.js +26 -0
- package/dist/src/gemini-web/client.js +328 -0
- package/dist/src/gemini-web/executor.js +285 -0
- package/dist/src/gemini-web/index.js +1 -0
- package/dist/src/gemini-web/types.js +1 -0
- package/dist/src/heartbeat.js +43 -0
- package/dist/src/mcp/server.js +40 -0
- package/dist/src/mcp/tools/consult.js +290 -0
- package/dist/src/mcp/tools/sessionResources.js +75 -0
- package/dist/src/mcp/tools/sessions.js +105 -0
- package/dist/src/mcp/types.js +22 -0
- package/dist/src/mcp/utils.js +37 -0
- package/dist/src/oracle/background.js +141 -0
- package/dist/src/oracle/claude.js +101 -0
- package/dist/src/oracle/client.js +197 -0
- package/dist/src/oracle/config.js +227 -0
- package/dist/src/oracle/errors.js +132 -0
- package/dist/src/oracle/files.js +378 -0
- package/dist/src/oracle/finishLine.js +32 -0
- package/dist/src/oracle/format.js +30 -0
- package/dist/src/oracle/fsAdapter.js +10 -0
- package/dist/src/oracle/gemini.js +195 -0
- package/dist/src/oracle/logging.js +36 -0
- package/dist/src/oracle/markdown.js +46 -0
- package/dist/src/oracle/modelResolver.js +183 -0
- package/dist/src/oracle/multiModelRunner.js +153 -0
- package/dist/src/oracle/oscProgress.js +24 -0
- package/dist/src/oracle/promptAssembly.js +13 -0
- package/dist/src/oracle/request.js +50 -0
- package/dist/src/oracle/run.js +596 -0
- package/dist/src/oracle/runUtils.js +31 -0
- package/dist/src/oracle/tokenEstimate.js +37 -0
- package/dist/src/oracle/tokenStats.js +39 -0
- package/dist/src/oracle/tokenStringifier.js +24 -0
- package/dist/src/oracle/types.js +1 -0
- package/dist/src/oracle.js +12 -0
- package/dist/src/oracleHome.js +13 -0
- package/dist/src/remote/client.js +129 -0
- package/dist/src/remote/health.js +113 -0
- package/dist/src/remote/remoteServiceConfig.js +31 -0
- package/dist/src/remote/server.js +533 -0
- package/dist/src/remote/types.js +1 -0
- package/dist/src/sessionManager.js +637 -0
- package/dist/src/sessionStore.js +56 -0
- package/dist/src/version.js +39 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/dist/vendor/oracle-notifier/README.md +24 -0
- package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
- package/package.json +115 -0
- package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
- package/vendor/oracle-notifier/README.md +24 -0
- package/vendor/oracle-notifier/build-notifier.sh +93 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, } from '../oracle.js';
|
|
5
|
+
import { runBrowserSessionExecution } from '../browser/sessionRunner.js';
|
|
6
|
+
import { renderMarkdownAnsi } from './markdownRenderer.js';
|
|
7
|
+
import { formatResponseMetadata, formatTransportMetadata } from './sessionDisplay.js';
|
|
8
|
+
import { markErrorLogged } from './errorUtils.js';
|
|
9
|
+
import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from './notifier.js';
|
|
10
|
+
import { sessionStore } from '../sessionStore.js';
|
|
11
|
+
import { wait } from '../sessionManager.js';
|
|
12
|
+
import { runMultiModelApiSession } from '../oracle/multiModelRunner.js';
|
|
13
|
+
import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from '../oracle/config.js';
|
|
14
|
+
import { isKnownModel } from '../oracle/modelResolver.js';
|
|
15
|
+
import { resolveModelConfig } from '../oracle/modelResolver.js';
|
|
16
|
+
import { buildPrompt, buildRequestBody } from '../oracle/request.js';
|
|
17
|
+
import { estimateRequestTokens } from '../oracle/tokenEstimate.js';
|
|
18
|
+
import { formatTokenEstimate, formatTokenValue } from '../oracle/runUtils.js';
|
|
19
|
+
import { formatFinishLine } from '../oracle/finishLine.js';
|
|
20
|
+
import { sanitizeOscProgress } from './oscUtils.js';
|
|
21
|
+
import { readFiles } from '../oracle/files.js';
|
|
22
|
+
import { cwd as getCwd } from 'node:process';
|
|
23
|
+
import { resumeBrowserSession } from '../browser/reattach.js';
|
|
24
|
+
import { estimateTokenCount } from '../browser/utils.js';
|
|
25
|
+
import { formatElapsed } from '../oracle/format.js';
|
|
26
|
+
const isTty = process.stdout.isTTY;
|
|
27
|
+
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
28
|
+
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
|
|
29
|
+
const writeInline = (chunk) => {
|
|
30
|
+
// Keep session logs intact while still echoing inline output to the user.
|
|
31
|
+
write(chunk);
|
|
32
|
+
return muteStdout ? true : process.stdout.write(chunk);
|
|
33
|
+
};
|
|
34
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
35
|
+
status: 'running',
|
|
36
|
+
startedAt: new Date().toISOString(),
|
|
37
|
+
mode,
|
|
38
|
+
...(browserConfig ? { browser: { config: browserConfig } } : {}),
|
|
39
|
+
});
|
|
40
|
+
const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
|
|
41
|
+
const modelForStatus = runOptions.model ?? sessionMeta.model;
|
|
42
|
+
try {
|
|
43
|
+
if (mode === 'browser') {
|
|
44
|
+
if (!browserConfig) {
|
|
45
|
+
throw new Error('Missing browser configuration for session.');
|
|
46
|
+
}
|
|
47
|
+
if (modelForStatus) {
|
|
48
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
49
|
+
status: 'running',
|
|
50
|
+
startedAt: new Date().toISOString(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const runnerDeps = {
|
|
54
|
+
...browserDeps,
|
|
55
|
+
persistRuntimeHint: async (runtime) => {
|
|
56
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
57
|
+
status: 'running',
|
|
58
|
+
browser: { config: browserConfig, runtime },
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const result = await runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, runnerDeps);
|
|
63
|
+
if (modelForStatus) {
|
|
64
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
65
|
+
status: 'completed',
|
|
66
|
+
completedAt: new Date().toISOString(),
|
|
67
|
+
usage: result.usage,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
71
|
+
status: 'completed',
|
|
72
|
+
completedAt: new Date().toISOString(),
|
|
73
|
+
usage: result.usage,
|
|
74
|
+
elapsedMs: result.elapsedMs,
|
|
75
|
+
browser: {
|
|
76
|
+
config: browserConfig,
|
|
77
|
+
runtime: result.runtime,
|
|
78
|
+
},
|
|
79
|
+
response: undefined,
|
|
80
|
+
transport: undefined,
|
|
81
|
+
error: undefined,
|
|
82
|
+
});
|
|
83
|
+
await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? '', log);
|
|
84
|
+
await sendSessionNotification({
|
|
85
|
+
sessionId: sessionMeta.id,
|
|
86
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
87
|
+
mode,
|
|
88
|
+
model: sessionMeta.model,
|
|
89
|
+
usage: result.usage,
|
|
90
|
+
characters: result.answerText?.length,
|
|
91
|
+
}, notificationSettings, log, result.answerText?.slice(0, 140));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const multiModels = Array.isArray(runOptions.models) ? runOptions.models.filter(Boolean) : [];
|
|
95
|
+
if (multiModels.length > 1) {
|
|
96
|
+
const [primaryModel] = multiModels;
|
|
97
|
+
if (!primaryModel) {
|
|
98
|
+
throw new Error('Missing model name for multi-model run.');
|
|
99
|
+
}
|
|
100
|
+
const modelConfig = await resolveModelConfig(primaryModel, {
|
|
101
|
+
baseUrl: runOptions.baseUrl,
|
|
102
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
103
|
+
});
|
|
104
|
+
const files = await readFiles(runOptions.file ?? [], { cwd });
|
|
105
|
+
const promptWithFiles = buildPrompt(runOptions.prompt, files, cwd);
|
|
106
|
+
const requestBody = buildRequestBody({
|
|
107
|
+
modelConfig,
|
|
108
|
+
systemPrompt: runOptions.system ?? DEFAULT_SYSTEM_PROMPT,
|
|
109
|
+
userPrompt: promptWithFiles,
|
|
110
|
+
searchEnabled: runOptions.search !== false,
|
|
111
|
+
maxOutputTokens: runOptions.maxOutput,
|
|
112
|
+
background: runOptions.background,
|
|
113
|
+
storeResponse: runOptions.background,
|
|
114
|
+
});
|
|
115
|
+
const estimatedTokens = estimateRequestTokens(requestBody, modelConfig);
|
|
116
|
+
const tokenLabel = formatTokenEstimate(estimatedTokens, (text) => (isTty ? kleur.green(text) : text));
|
|
117
|
+
const filesPhrase = files.length === 0 ? 'no files' : `${files.length} files`;
|
|
118
|
+
const modelsLabel = multiModels.join(', ');
|
|
119
|
+
log(`Calling ${isTty ? kleur.cyan(modelsLabel) : modelsLabel} — ${tokenLabel} tokens, ${filesPhrase}.`);
|
|
120
|
+
const multiRunTips = [];
|
|
121
|
+
if (files.length === 0) {
|
|
122
|
+
multiRunTips.push('Tip: no files attached — Oracle works best with project context. Add files via --file path/to/code or docs.');
|
|
123
|
+
}
|
|
124
|
+
const shortPrompt = (runOptions.prompt?.trim().length ?? 0) < 80;
|
|
125
|
+
if (shortPrompt) {
|
|
126
|
+
multiRunTips.push('Tip: brief prompts often yield generic answers — aim for 6–30 sentences and attach key files.');
|
|
127
|
+
}
|
|
128
|
+
for (const tip of multiRunTips) {
|
|
129
|
+
log(dim(tip));
|
|
130
|
+
}
|
|
131
|
+
// Surface long-running model expectations up front so users know why a response might lag.
|
|
132
|
+
const longRunningModels = multiModels.filter((model) => isKnownModel(model) && MODEL_CONFIGS[model]?.reasoning?.effort === 'high');
|
|
133
|
+
if (longRunningModels.length > 0) {
|
|
134
|
+
for (const model of longRunningModels) {
|
|
135
|
+
log('');
|
|
136
|
+
const headingLabel = `[${model}]`;
|
|
137
|
+
log(isTty ? kleur.bold(headingLabel) : headingLabel);
|
|
138
|
+
log(dim('This model can take up to 60 minutes (usually replies much faster).'));
|
|
139
|
+
log(dim('Press Ctrl+C to cancel.'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const shouldStreamInline = !muteStdout && process.stdout.isTTY;
|
|
143
|
+
const shouldRenderMarkdown = shouldStreamInline && runOptions.renderPlain !== true;
|
|
144
|
+
const printedModels = new Set();
|
|
145
|
+
const answerFallbacks = new Map();
|
|
146
|
+
const stripOscProgress = (text) => sanitizeOscProgress(text, shouldStreamInline);
|
|
147
|
+
const printModelLog = async (model) => {
|
|
148
|
+
if (printedModels.has(model))
|
|
149
|
+
return;
|
|
150
|
+
printedModels.add(model);
|
|
151
|
+
const body = stripOscProgress(await sessionStore.readModelLog(sessionMeta.id, model));
|
|
152
|
+
log('');
|
|
153
|
+
const fallback = answerFallbacks.get(model);
|
|
154
|
+
const hasBody = body.length > 0;
|
|
155
|
+
if (!hasBody && !fallback) {
|
|
156
|
+
log(dim(`${model}: (no output recorded)`));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const headingLabel = `[${model}]`;
|
|
160
|
+
const heading = shouldStreamInline ? kleur.bold(headingLabel) : headingLabel;
|
|
161
|
+
log(heading);
|
|
162
|
+
const content = hasBody ? body : fallback ?? '';
|
|
163
|
+
const printable = shouldRenderMarkdown ? renderMarkdownAnsi(content) : content;
|
|
164
|
+
writeInline(printable);
|
|
165
|
+
if (!printable.endsWith('\n')) {
|
|
166
|
+
log('');
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const summary = await runMultiModelApiSession({
|
|
170
|
+
sessionMeta,
|
|
171
|
+
runOptions,
|
|
172
|
+
models: multiModels,
|
|
173
|
+
cwd,
|
|
174
|
+
version,
|
|
175
|
+
onModelDone: shouldStreamInline
|
|
176
|
+
? async (result) => {
|
|
177
|
+
if (result.answerText) {
|
|
178
|
+
answerFallbacks.set(result.model, result.answerText);
|
|
179
|
+
}
|
|
180
|
+
await printModelLog(result.model);
|
|
181
|
+
}
|
|
182
|
+
: undefined,
|
|
183
|
+
}, {
|
|
184
|
+
runOracleImpl: muteStdout
|
|
185
|
+
? (opts, deps) => runOracle(opts, { ...deps, allowStdout: false })
|
|
186
|
+
: undefined,
|
|
187
|
+
});
|
|
188
|
+
if (!shouldStreamInline) {
|
|
189
|
+
// If we couldn't stream inline (e.g., non-TTY), print all logs after completion.
|
|
190
|
+
for (const [index, result] of summary.fulfilled.entries()) {
|
|
191
|
+
if (index > 0) {
|
|
192
|
+
log('');
|
|
193
|
+
}
|
|
194
|
+
await printModelLog(result.model);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const aggregateUsage = summary.fulfilled.reduce((acc, entry) => ({
|
|
198
|
+
inputTokens: acc.inputTokens + entry.usage.inputTokens,
|
|
199
|
+
outputTokens: acc.outputTokens + entry.usage.outputTokens,
|
|
200
|
+
reasoningTokens: acc.reasoningTokens + entry.usage.reasoningTokens,
|
|
201
|
+
totalTokens: acc.totalTokens + entry.usage.totalTokens,
|
|
202
|
+
cost: (acc.cost ?? 0) + (entry.usage.cost ?? 0),
|
|
203
|
+
}), { inputTokens: 0, outputTokens: 0, reasoningTokens: 0, totalTokens: 0, cost: 0 });
|
|
204
|
+
const tokensDisplay = [
|
|
205
|
+
aggregateUsage.inputTokens,
|
|
206
|
+
aggregateUsage.outputTokens,
|
|
207
|
+
aggregateUsage.reasoningTokens,
|
|
208
|
+
aggregateUsage.totalTokens,
|
|
209
|
+
]
|
|
210
|
+
.map((v, idx) => formatTokenValue(v, {
|
|
211
|
+
input_tokens: aggregateUsage.inputTokens,
|
|
212
|
+
output_tokens: aggregateUsage.outputTokens,
|
|
213
|
+
reasoning_tokens: aggregateUsage.reasoningTokens,
|
|
214
|
+
total_tokens: aggregateUsage.totalTokens,
|
|
215
|
+
}, idx))
|
|
216
|
+
.join('/');
|
|
217
|
+
const tokensPart = (() => {
|
|
218
|
+
const parts = tokensDisplay.split('/');
|
|
219
|
+
if (parts.length !== 4)
|
|
220
|
+
return tokensDisplay;
|
|
221
|
+
return `↑${parts[0]} ↓${parts[1]} ↻${parts[2]} Δ${parts[3]}`;
|
|
222
|
+
})();
|
|
223
|
+
const statusColor = summary.rejected.length === 0 ? kleur.green : summary.fulfilled.length > 0 ? kleur.yellow : kleur.red;
|
|
224
|
+
const overallText = `${summary.fulfilled.length}/${multiModels.length} models`;
|
|
225
|
+
const { line1 } = formatFinishLine({
|
|
226
|
+
elapsedMs: summary.elapsedMs,
|
|
227
|
+
model: overallText,
|
|
228
|
+
costUsd: aggregateUsage.cost ?? null,
|
|
229
|
+
tokensPart,
|
|
230
|
+
});
|
|
231
|
+
log(statusColor(line1));
|
|
232
|
+
const hasFailure = summary.rejected.length > 0;
|
|
233
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
234
|
+
status: hasFailure ? 'error' : 'completed',
|
|
235
|
+
completedAt: new Date().toISOString(),
|
|
236
|
+
usage: aggregateUsage,
|
|
237
|
+
elapsedMs: summary.elapsedMs,
|
|
238
|
+
response: undefined,
|
|
239
|
+
transport: undefined,
|
|
240
|
+
error: undefined,
|
|
241
|
+
});
|
|
242
|
+
const totalCharacters = summary.fulfilled.reduce((sum, entry) => sum + entry.answerText.length, 0);
|
|
243
|
+
await sendSessionNotification({
|
|
244
|
+
sessionId: sessionMeta.id,
|
|
245
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
246
|
+
mode,
|
|
247
|
+
model: `${multiModels.length} models`,
|
|
248
|
+
usage: aggregateUsage,
|
|
249
|
+
characters: totalCharacters,
|
|
250
|
+
}, notificationSettings, log);
|
|
251
|
+
if (runOptions.writeOutputPath) {
|
|
252
|
+
const savedOutputs = [];
|
|
253
|
+
for (const entry of summary.fulfilled) {
|
|
254
|
+
const modelOutputPath = deriveModelOutputPath(runOptions.writeOutputPath, entry.model);
|
|
255
|
+
const savedPath = await writeAssistantOutput(modelOutputPath, entry.answerText, log);
|
|
256
|
+
if (savedPath) {
|
|
257
|
+
savedOutputs.push({ model: entry.model, path: savedPath });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (savedOutputs.length > 0) {
|
|
261
|
+
log(dim('Saved outputs:'));
|
|
262
|
+
for (const item of savedOutputs) {
|
|
263
|
+
log(dim(`- ${item.model} -> ${item.path}`));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (hasFailure) {
|
|
268
|
+
throw summary.rejected[0].reason;
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const singleModelOverride = multiModels.length === 1 ? multiModels[0] : undefined;
|
|
273
|
+
const apiRunOptions = singleModelOverride
|
|
274
|
+
? { ...runOptions, model: singleModelOverride, models: undefined }
|
|
275
|
+
: runOptions;
|
|
276
|
+
if (modelForStatus && singleModelOverride == null) {
|
|
277
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
278
|
+
status: 'running',
|
|
279
|
+
startedAt: new Date().toISOString(),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const result = await runOracle(apiRunOptions, {
|
|
283
|
+
cwd,
|
|
284
|
+
log,
|
|
285
|
+
write,
|
|
286
|
+
allowStdout: !muteStdout,
|
|
287
|
+
});
|
|
288
|
+
if (result.mode !== 'live') {
|
|
289
|
+
throw new Error('Unexpected preview result while running a session.');
|
|
290
|
+
}
|
|
291
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
292
|
+
status: 'completed',
|
|
293
|
+
completedAt: new Date().toISOString(),
|
|
294
|
+
usage: result.usage,
|
|
295
|
+
elapsedMs: result.elapsedMs,
|
|
296
|
+
response: extractResponseMetadata(result.response),
|
|
297
|
+
transport: undefined,
|
|
298
|
+
error: undefined,
|
|
299
|
+
});
|
|
300
|
+
if (modelForStatus && singleModelOverride == null) {
|
|
301
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
302
|
+
status: 'completed',
|
|
303
|
+
completedAt: new Date().toISOString(),
|
|
304
|
+
usage: result.usage,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
const answerText = extractTextOutput(result.response);
|
|
308
|
+
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
309
|
+
await sendSessionNotification({
|
|
310
|
+
sessionId: sessionMeta.id,
|
|
311
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
312
|
+
mode,
|
|
313
|
+
model: sessionMeta.model ?? runOptions.model,
|
|
314
|
+
usage: result.usage,
|
|
315
|
+
characters: answerText.length,
|
|
316
|
+
}, notificationSettings, log, answerText.slice(0, 140));
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
const message = formatError(error);
|
|
320
|
+
log(`ERROR: ${message}`);
|
|
321
|
+
markErrorLogged(error);
|
|
322
|
+
const userError = asOracleUserError(error);
|
|
323
|
+
const connectionLost = userError?.category === 'browser-automation' && userError.details?.stage === 'connection-lost';
|
|
324
|
+
const assistantTimeout = userError?.category === 'browser-automation' && userError.details?.stage === 'assistant-timeout';
|
|
325
|
+
if (connectionLost && mode === 'browser') {
|
|
326
|
+
const runtime = userError.details?.runtime;
|
|
327
|
+
log(dim('Chrome disconnected before completion; keeping session running for reattach.'));
|
|
328
|
+
if (modelForStatus) {
|
|
329
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
330
|
+
status: 'running',
|
|
331
|
+
completedAt: undefined,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
335
|
+
status: 'running',
|
|
336
|
+
errorMessage: message,
|
|
337
|
+
mode,
|
|
338
|
+
browser: {
|
|
339
|
+
config: browserConfig,
|
|
340
|
+
runtime: runtime ?? sessionMeta.browser?.runtime,
|
|
341
|
+
},
|
|
342
|
+
response: { status: 'running', incompleteReason: 'chrome-disconnected' },
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (assistantTimeout && mode === 'browser') {
|
|
347
|
+
const runtime = userError.details?.runtime;
|
|
348
|
+
log(dim('Assistant response timed out; keeping session running for reattach.'));
|
|
349
|
+
if (modelForStatus) {
|
|
350
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
351
|
+
status: 'running',
|
|
352
|
+
completedAt: undefined,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
356
|
+
status: 'running',
|
|
357
|
+
errorMessage: message,
|
|
358
|
+
mode,
|
|
359
|
+
browser: {
|
|
360
|
+
config: browserConfig,
|
|
361
|
+
runtime: runtime ?? sessionMeta.browser?.runtime,
|
|
362
|
+
},
|
|
363
|
+
response: { status: 'running', incompleteReason: 'assistant-timeout' },
|
|
364
|
+
});
|
|
365
|
+
const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
366
|
+
if (autoReattachIntervalMs > 0) {
|
|
367
|
+
const autoRuntime = runtime ?? sessionMeta.browser?.runtime;
|
|
368
|
+
const success = await autoReattachUntilComplete({
|
|
369
|
+
sessionMeta,
|
|
370
|
+
runtime: autoRuntime ?? undefined,
|
|
371
|
+
browserConfig,
|
|
372
|
+
runOptions,
|
|
373
|
+
modelForStatus,
|
|
374
|
+
notificationSettings,
|
|
375
|
+
log,
|
|
376
|
+
});
|
|
377
|
+
if (success) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
log(dim(`Reattach later with: oracle session ${sessionMeta.id}`));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (userError) {
|
|
385
|
+
log(dim(`User error (${userError.category}): ${userError.message}`));
|
|
386
|
+
}
|
|
387
|
+
const responseMetadata = error instanceof OracleResponseError ? error.metadata : undefined;
|
|
388
|
+
const metadataLine = formatResponseMetadata(responseMetadata);
|
|
389
|
+
if (metadataLine) {
|
|
390
|
+
log(dim(`Response metadata: ${metadataLine}`));
|
|
391
|
+
}
|
|
392
|
+
const transportMetadata = error instanceof OracleTransportError ? { reason: error.reason } : undefined;
|
|
393
|
+
const transportLine = formatTransportMetadata(transportMetadata);
|
|
394
|
+
if (transportLine) {
|
|
395
|
+
log(dim(`Transport: ${transportLine}`));
|
|
396
|
+
}
|
|
397
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
398
|
+
status: 'error',
|
|
399
|
+
completedAt: new Date().toISOString(),
|
|
400
|
+
errorMessage: message,
|
|
401
|
+
mode,
|
|
402
|
+
browser: browserConfig ? { config: browserConfig } : undefined,
|
|
403
|
+
response: responseMetadata,
|
|
404
|
+
transport: transportMetadata,
|
|
405
|
+
error: userError
|
|
406
|
+
? {
|
|
407
|
+
category: userError.category,
|
|
408
|
+
message: userError.message,
|
|
409
|
+
details: userError.details,
|
|
410
|
+
}
|
|
411
|
+
: undefined,
|
|
412
|
+
});
|
|
413
|
+
if (modelForStatus) {
|
|
414
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
415
|
+
status: 'error',
|
|
416
|
+
completedAt: new Date().toISOString(),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function formatError(error) {
|
|
423
|
+
return error instanceof Error ? error.message : String(error);
|
|
424
|
+
}
|
|
425
|
+
async function writeAssistantOutput(targetPath, content, log) {
|
|
426
|
+
if (!targetPath)
|
|
427
|
+
return;
|
|
428
|
+
if (!content || content.trim().length === 0) {
|
|
429
|
+
log(dim('write-output skipped: no assistant content to save.'));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
433
|
+
const normalizedSessionsDir = path.resolve(sessionStore.sessionsDir());
|
|
434
|
+
if (normalizedTarget === normalizedSessionsDir ||
|
|
435
|
+
normalizedTarget.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
436
|
+
log(dim(`write-output skipped: refusing to write inside session storage (${normalizedSessionsDir}).`));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
await fs.mkdir(path.dirname(normalizedTarget), { recursive: true });
|
|
441
|
+
const payload = content.endsWith('\n') ? content : `${content}\n`;
|
|
442
|
+
await fs.writeFile(normalizedTarget, payload, 'utf8');
|
|
443
|
+
log(dim(`Saved assistant output to ${normalizedTarget}`));
|
|
444
|
+
return normalizedTarget;
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
448
|
+
if (isPermissionError(error)) {
|
|
449
|
+
const fallbackPath = buildFallbackPath(normalizedTarget);
|
|
450
|
+
if (fallbackPath) {
|
|
451
|
+
try {
|
|
452
|
+
await fs.mkdir(path.dirname(fallbackPath), { recursive: true });
|
|
453
|
+
const payload = content.endsWith('\n') ? content : `${content}\n`;
|
|
454
|
+
await fs.writeFile(fallbackPath, payload, 'utf8');
|
|
455
|
+
log(dim(`write-output fallback to ${fallbackPath} (original failed: ${reason})`));
|
|
456
|
+
return fallbackPath;
|
|
457
|
+
}
|
|
458
|
+
catch (innerError) {
|
|
459
|
+
const innerReason = innerError instanceof Error ? innerError.message : String(innerError);
|
|
460
|
+
log(dim(`write-output failed (${reason}); fallback failed (${innerReason}); session completed anyway.`));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, runOptions, modelForStatus, notificationSettings, log, }) {
|
|
469
|
+
if (!runtime || !browserConfig) {
|
|
470
|
+
log(dim('Auto-reattach disabled: missing runtime or browser config.'));
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const delayMs = Math.max(0, browserConfig.autoReattachDelayMs ?? 0);
|
|
474
|
+
const intervalMs = Math.max(0, browserConfig.autoReattachIntervalMs ?? 0);
|
|
475
|
+
if (intervalMs <= 0) {
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const timeoutMs = Math.max(0, browserConfig.autoReattachTimeoutMs ?? 0) ||
|
|
479
|
+
Math.max(0, browserConfig.timeoutMs ?? 0) ||
|
|
480
|
+
120_000;
|
|
481
|
+
const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
|
|
482
|
+
const maxDeadline = Date.now() + maxTotalMs;
|
|
483
|
+
if (delayMs > 0) {
|
|
484
|
+
log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
|
|
485
|
+
await wait(delayMs);
|
|
486
|
+
}
|
|
487
|
+
log(dim(`Auto-reattach will stop after ${formatElapsed(maxTotalMs)} if no answer is captured.`));
|
|
488
|
+
const logger = ((message) => {
|
|
489
|
+
if (message) {
|
|
490
|
+
log(dim(message));
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
logger.verbose = true;
|
|
494
|
+
let attempt = 0;
|
|
495
|
+
for (;;) {
|
|
496
|
+
const remainingBudgetMs = maxDeadline - Date.now();
|
|
497
|
+
if (remainingBudgetMs <= 0) {
|
|
498
|
+
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
attempt += 1;
|
|
502
|
+
log(dim(`Auto-reattach attempt ${attempt}...`));
|
|
503
|
+
try {
|
|
504
|
+
const reattachConfig = {
|
|
505
|
+
...browserConfig,
|
|
506
|
+
timeoutMs,
|
|
507
|
+
};
|
|
508
|
+
const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
|
|
509
|
+
promptPreview: sessionMeta.promptPreview,
|
|
510
|
+
});
|
|
511
|
+
const answerText = result.answerMarkdown || result.answerText || '';
|
|
512
|
+
const outputTokens = estimateTokenCount(answerText);
|
|
513
|
+
const logWriter = sessionStore.createLogWriter(sessionMeta.id);
|
|
514
|
+
logWriter.logLine(`[auto-reattach] captured assistant response on attempt ${attempt}`);
|
|
515
|
+
logWriter.logLine('Answer:');
|
|
516
|
+
logWriter.logLine(answerText);
|
|
517
|
+
logWriter.stream.end();
|
|
518
|
+
if (modelForStatus) {
|
|
519
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
520
|
+
status: 'completed',
|
|
521
|
+
completedAt: new Date().toISOString(),
|
|
522
|
+
usage: {
|
|
523
|
+
inputTokens: 0,
|
|
524
|
+
outputTokens,
|
|
525
|
+
reasoningTokens: 0,
|
|
526
|
+
totalTokens: outputTokens,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
531
|
+
status: 'completed',
|
|
532
|
+
completedAt: new Date().toISOString(),
|
|
533
|
+
usage: {
|
|
534
|
+
inputTokens: 0,
|
|
535
|
+
outputTokens,
|
|
536
|
+
reasoningTokens: 0,
|
|
537
|
+
totalTokens: outputTokens,
|
|
538
|
+
},
|
|
539
|
+
browser: {
|
|
540
|
+
config: browserConfig,
|
|
541
|
+
runtime,
|
|
542
|
+
},
|
|
543
|
+
response: { status: 'completed' },
|
|
544
|
+
error: undefined,
|
|
545
|
+
transport: undefined,
|
|
546
|
+
});
|
|
547
|
+
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
548
|
+
await sendSessionNotification({
|
|
549
|
+
sessionId: sessionMeta.id,
|
|
550
|
+
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
551
|
+
mode: sessionMeta.mode ?? 'browser',
|
|
552
|
+
model: sessionMeta.model ?? runOptions.model,
|
|
553
|
+
usage: {
|
|
554
|
+
inputTokens: 0,
|
|
555
|
+
outputTokens,
|
|
556
|
+
},
|
|
557
|
+
characters: answerText.length,
|
|
558
|
+
}, notificationSettings, log, answerText.slice(0, 140));
|
|
559
|
+
log(kleur.green('Auto-reattach succeeded; session marked completed.'));
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
564
|
+
log(dim(`Auto-reattach attempt ${attempt} failed: ${message}`));
|
|
565
|
+
}
|
|
566
|
+
const remainingAfterAttemptMs = maxDeadline - Date.now();
|
|
567
|
+
if (remainingAfterAttemptMs <= 0) {
|
|
568
|
+
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
await wait(Math.min(intervalMs, remainingAfterAttemptMs));
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
export function deriveModelOutputPath(basePath, model) {
|
|
575
|
+
if (!basePath)
|
|
576
|
+
return undefined;
|
|
577
|
+
const ext = path.extname(basePath);
|
|
578
|
+
const stem = path.basename(basePath, ext);
|
|
579
|
+
const dir = path.dirname(basePath);
|
|
580
|
+
const suffix = ext.length > 0 ? `${stem}.${model}${ext}` : `${stem}.${model}`;
|
|
581
|
+
return path.join(dir, suffix);
|
|
582
|
+
}
|
|
583
|
+
function isPermissionError(error) {
|
|
584
|
+
if (!(error instanceof Error))
|
|
585
|
+
return false;
|
|
586
|
+
const code = error.code;
|
|
587
|
+
return code === 'EACCES' || code === 'EPERM';
|
|
588
|
+
}
|
|
589
|
+
function buildFallbackPath(original) {
|
|
590
|
+
const ext = path.extname(original);
|
|
591
|
+
const stem = path.basename(original, ext);
|
|
592
|
+
const dir = getCwd();
|
|
593
|
+
const candidate = ext ? `${stem}.fallback${ext}` : `${stem}.fallback`;
|
|
594
|
+
const fallback = path.join(dir, candidate);
|
|
595
|
+
const normalizedSessionsDir = path.resolve(sessionStore.sessionsDir());
|
|
596
|
+
const normalizedFallback = path.resolve(fallback);
|
|
597
|
+
if (normalizedFallback === normalizedSessionsDir ||
|
|
598
|
+
normalizedFallback.startsWith(`${normalizedSessionsDir}${path.sep}`)) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
return fallback;
|
|
602
|
+
}
|