@steipete/oracle 0.20.0 → 0.20.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.
- package/dist/bin/oracle-cli.js +61 -16
- package/dist/src/browser/actions/thinkingTime.js +11 -5
- package/dist/src/browser/chromeLifecycle.js +23 -12
- package/dist/src/browser/index.js +33 -6
- package/dist/src/browser/liveTabs.js +8 -0
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/reattach.js +157 -108
- package/dist/src/browser/recoveryTarget.js +31 -6
- package/dist/src/browser/sessionRunner.js +30 -7
- package/dist/src/browser/targetClaim.js +2 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +35 -3
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/sessionDisplay.js +11 -3
- package/dist/src/cli/sessionRunner.js +150 -37
- package/dist/src/remote/server.js +1 -0
- package/dist/src/sessionManager.js +8 -1
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +9 -9
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -2,6 +2,8 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { sessionStore } from "../sessionStore.js";
|
|
5
|
+
import { resolveBrowserConfig } from "../browser/config.js";
|
|
6
|
+
import { browserPromptFingerprint } from "../browser/promptFingerprint.js";
|
|
5
7
|
import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
|
|
6
8
|
import { isRecoveredConversationHarvestReady, recoverConversationTab, } from "../browser/recoverConversation.js";
|
|
7
9
|
import { resolveOutputPath } from "./writeOutputPath.js";
|
|
@@ -9,6 +11,7 @@ import { persistBrowserHarvest } from "./harvestIntegrity.js";
|
|
|
9
11
|
import { completeOwnedBrowserHarvest } from "./recoveredBrowserHarvest.js";
|
|
10
12
|
const LIVE_POLL_MS = 2000;
|
|
11
13
|
const DEFAULT_STALL_THRESHOLD_MS = 60_000;
|
|
14
|
+
const HARVEST_FRESHNESS_POLL_MS = 250;
|
|
12
15
|
function isRecoverableMissingTabError(message) {
|
|
13
16
|
return (message.includes("No ChatGPT tab matched") ||
|
|
14
17
|
message.includes("No live ChatGPT tabs found") ||
|
|
@@ -31,6 +34,35 @@ function finishRecoveredChrome(recoveredChrome, closeAfterRecover) {
|
|
|
31
34
|
// best-effort cleanup
|
|
32
35
|
}
|
|
33
36
|
}
|
|
37
|
+
function harvestMatchesSessionPrompt(harvested, fingerprint) {
|
|
38
|
+
const answer = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText;
|
|
39
|
+
if (harvested.assistantFollowsLatestUser !== true || !answer?.trim())
|
|
40
|
+
return false;
|
|
41
|
+
return (fingerprint === undefined ||
|
|
42
|
+
(typeof harvested.lastUserMessageId === "string" &&
|
|
43
|
+
harvested.lastUserMessageId.trim().length > 0 &&
|
|
44
|
+
browserPromptFingerprint(harvested.lastUserTextRaw ?? harvested.lastUserText, harvested.lastUserMessageId) === fingerprint));
|
|
45
|
+
}
|
|
46
|
+
async function harvestSessionPrompt(meta, options, requireSessionPrompt = true) {
|
|
47
|
+
const fingerprint = requireSessionPrompt ? meta.browser?.runtime?.submittedPromptHash : undefined;
|
|
48
|
+
if (fingerprint === null) {
|
|
49
|
+
throw new Error("This browser session has no confirmed submitted user turn; retry after submission or use --browser-tab to inspect a specific tab.");
|
|
50
|
+
}
|
|
51
|
+
if (requireSessionPrompt && fingerprint === undefined) {
|
|
52
|
+
console.warn("Legacy browser session: submitted-turn identity is unavailable; verifying only latest user/assistant pairing.");
|
|
53
|
+
}
|
|
54
|
+
const freshnessTimeoutMs = resolveBrowserConfig(meta.browser?.config).inputTimeoutMs;
|
|
55
|
+
const deadline = Date.now() + freshnessTimeoutMs;
|
|
56
|
+
let harvested = await harvestChatGptTab(options);
|
|
57
|
+
while (!harvestMatchesSessionPrompt(harvested, fingerprint) && Date.now() < deadline) {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, HARVEST_FRESHNESS_POLL_MS));
|
|
59
|
+
harvested = await harvestChatGptTab(options);
|
|
60
|
+
}
|
|
61
|
+
if (!harvestMatchesSessionPrompt(harvested, fingerprint)) {
|
|
62
|
+
throw new Error(`Latest ChatGPT turn did not contain an assistant answer paired with this session prompt after ${Math.ceil(freshnessTimeoutMs / 1000)}s; refusing to harvest stale output.`);
|
|
63
|
+
}
|
|
64
|
+
return harvested;
|
|
65
|
+
}
|
|
34
66
|
function sessionBrowserEndpoint(meta) {
|
|
35
67
|
const runtime = meta?.browser?.runtime ?? {};
|
|
36
68
|
const remote = meta?.browser?.config?.remoteChrome ?? {};
|
|
@@ -167,12 +199,12 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
167
199
|
try {
|
|
168
200
|
let harvested;
|
|
169
201
|
try {
|
|
170
|
-
harvested = await
|
|
202
|
+
harvested = await harvestSessionPrompt(meta, {
|
|
171
203
|
host: initialEndpoint.host,
|
|
172
204
|
port: initialEndpoint.port,
|
|
173
205
|
ref,
|
|
174
206
|
stallWindowMs: options.stallWindowMs,
|
|
175
|
-
});
|
|
207
|
+
}, !options.browserTabRef);
|
|
176
208
|
}
|
|
177
209
|
catch (error) {
|
|
178
210
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -184,7 +216,7 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
184
216
|
existingEndpoint: recordedEndpoint ?? undefined,
|
|
185
217
|
});
|
|
186
218
|
recoveredChrome = recovered.chrome;
|
|
187
|
-
harvested = await
|
|
219
|
+
harvested = await harvestSessionPrompt(meta, {
|
|
188
220
|
host: recovered.host,
|
|
189
221
|
port: recovered.port,
|
|
190
222
|
ref: recovered.ref,
|
package/dist/src/cli/detach.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isProModel } from "../oracle/modelResolver.js";
|
|
2
|
+
import { isGpt6ProAlias } from "./browserConfig.js";
|
|
2
3
|
export function shouldDetachSession({
|
|
3
4
|
// Params kept for policy tweaks.
|
|
4
5
|
engine, model, reasoningMode, waitPreference, disableDetachEnv, }) {
|
|
@@ -7,7 +8,7 @@ engine, model, reasoningMode, waitPreference, disableDetachEnv, }) {
|
|
|
7
8
|
// Keep long local browser Pro work in a separate process even while the CLI
|
|
8
9
|
// stays attached to its session log. If the foreground stream is interrupted,
|
|
9
10
|
// the worker can still finish the browser run and persist the answer.
|
|
10
|
-
if (engine === "browser" && isProModel(model))
|
|
11
|
+
if (engine === "browser" && (isProModel(model) || isGpt6ProAlias(model)))
|
|
11
12
|
return true;
|
|
12
13
|
// For API runs, explicit --wait keeps execution in the foreground.
|
|
13
14
|
if (waitPreference)
|
|
@@ -17,6 +18,14 @@ engine, model, reasoningMode, waitPreference, disableDetachEnv, }) {
|
|
|
17
18
|
return true;
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
21
|
+
export function shouldExitAfterTopLevelSigint(remainingListenerCount) {
|
|
22
|
+
return remainingListenerCount === 0;
|
|
23
|
+
}
|
|
24
|
+
export function detachedCancellationExitCode(cancelled, finalStatus, currentExitCode) {
|
|
25
|
+
if (!cancelled)
|
|
26
|
+
return currentExitCode;
|
|
27
|
+
return finalStatus === "completed" || finalStatus === "partial" ? 0 : 130;
|
|
28
|
+
}
|
|
20
29
|
export function stopDetachedWorker(workerPid, kill = process.kill) {
|
|
21
30
|
try {
|
|
22
31
|
kill(workerPid, "SIGTERM");
|
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
const CANCELLATION_MARKER = ".cancel-requested";
|
|
7
|
+
export function detachedSessionCancellationPath(sessionDir, workerPid) {
|
|
8
|
+
return path.join(sessionDir, `${CANCELLATION_MARKER}-${workerPid}`);
|
|
9
|
+
}
|
|
10
|
+
export async function requestDetachedSessionCancellation(markerPath) {
|
|
11
|
+
await writeFile(markerPath, "cancel\n", "utf8");
|
|
12
|
+
}
|
|
13
|
+
export async function clearDetachedSessionCancellation(markerPath) {
|
|
14
|
+
await rm(markerPath, { force: true });
|
|
15
|
+
}
|
|
16
|
+
export async function waitForDetachedSessionCancellation({ markerPath, signal, pollIntervalMs = 100, }) {
|
|
17
|
+
while (!signal.aborted) {
|
|
18
|
+
try {
|
|
19
|
+
await access(markerPath);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!(error instanceof Error) || error.code !== "ENOENT") {
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await delay(pollIntervalMs, undefined, { signal });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (signal.aborted && error instanceof Error && error.name === "AbortError") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
3
39
|
export function resolveOracleCliEntrypoint(moduleUrl = import.meta.url) {
|
|
4
40
|
const extension = fileURLToPath(moduleUrl).endsWith(".ts") ? "ts" : "js";
|
|
5
41
|
return fileURLToPath(new URL(`../../bin/oracle-cli.${extension}`, moduleUrl));
|
|
@@ -359,7 +359,10 @@ export async function attachSession(sessionId, options) {
|
|
|
359
359
|
console.log(dim(`User error: ${userErrorSummary}`));
|
|
360
360
|
}
|
|
361
361
|
}
|
|
362
|
-
const shouldTrimIntro = initialStatus === "completed" ||
|
|
362
|
+
const shouldTrimIntro = initialStatus === "completed" ||
|
|
363
|
+
initialStatus === "partial" ||
|
|
364
|
+
initialStatus === "error" ||
|
|
365
|
+
initialStatus === "cancelled";
|
|
363
366
|
if (options?.renderPrompt !== false) {
|
|
364
367
|
const prompt = await readStoredPrompt(sessionId);
|
|
365
368
|
if (prompt) {
|
|
@@ -485,7 +488,10 @@ export async function attachSession(sessionId, options) {
|
|
|
485
488
|
if (!latest) {
|
|
486
489
|
break;
|
|
487
490
|
}
|
|
488
|
-
if (latest.status === "completed" ||
|
|
491
|
+
if (latest.status === "completed" ||
|
|
492
|
+
latest.status === "partial" ||
|
|
493
|
+
latest.status === "error" ||
|
|
494
|
+
latest.status === "cancelled") {
|
|
489
495
|
await printNew();
|
|
490
496
|
flushRemainder();
|
|
491
497
|
if (!options?.suppressMetadata) {
|
|
@@ -516,7 +522,9 @@ export async function attachSession(sessionId, options) {
|
|
|
516
522
|
if (!settled) {
|
|
517
523
|
break;
|
|
518
524
|
}
|
|
519
|
-
if (settled.status === "completed" ||
|
|
525
|
+
if (settled.status === "completed" ||
|
|
526
|
+
settled.status === "partial" ||
|
|
527
|
+
settled.status === "cancelled") {
|
|
520
528
|
continue;
|
|
521
529
|
}
|
|
522
530
|
await printNew();
|
|
@@ -9,7 +9,6 @@ import { formatResponseMetadata, formatTransportMetadata } from "./sessionDispla
|
|
|
9
9
|
import { markErrorLogged } from "./errorUtils.js";
|
|
10
10
|
import { sendSessionNotification, deriveNotificationSettingsFromMetadata, } from "./notifier.js";
|
|
11
11
|
import { sessionStore } from "../sessionStore.js";
|
|
12
|
-
import { wait } from "../sessionManager.js";
|
|
13
12
|
import { runMultiModelApiSession } from "../oracle/multiModelRunner.js";
|
|
14
13
|
import { MODEL_CONFIGS, DEFAULT_SYSTEM_PROMPT } from "../oracle/config.js";
|
|
15
14
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
@@ -22,31 +21,71 @@ import { sanitizeOscProgress } from "./oscUtils.js";
|
|
|
22
21
|
import { readFiles } from "../oracle/files.js";
|
|
23
22
|
import { cwd as getCwd } from "node:process";
|
|
24
23
|
import { resumeBrowserSession } from "../browser/reattach.js";
|
|
25
|
-
import { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
|
|
24
|
+
import { recoveryCaptureFromRuntime, retireCancelledBrowserTarget, retireRecoveredBrowserTarget, } from "../browser/recoveryTarget.js";
|
|
26
25
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
27
|
-
import { estimateTokenCount } from "../browser/utils.js";
|
|
26
|
+
import { delay, estimateTokenCount } from "../browser/utils.js";
|
|
28
27
|
import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
|
|
29
28
|
import { formatElapsed } from "../oracle/format.js";
|
|
30
29
|
import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
|
|
30
|
+
import { BrowserRunCancelledError } from "../oracle/errors.js";
|
|
31
31
|
const isTty = process.stdout.isTTY;
|
|
32
32
|
const dim = (text) => (isTty ? kleur.dim(text) : text);
|
|
33
|
-
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, }) {
|
|
33
|
+
export async function performSessionRun({ sessionMeta, runOptions, mode, browserConfig, cwd, log, write, version, notifications, browserDeps, muteStdout = false, signal, }) {
|
|
34
34
|
const writeInline = (chunk) => {
|
|
35
35
|
// Keep session logs intact while still echoing inline output to the user.
|
|
36
36
|
write(chunk);
|
|
37
37
|
return muteStdout ? true : process.stdout.write(chunk);
|
|
38
38
|
};
|
|
39
39
|
let currentBrowser = browserConfig
|
|
40
|
-
? {
|
|
40
|
+
? {
|
|
41
|
+
config: browserConfig,
|
|
42
|
+
...(mode === "browser" && sessionMeta.browser?.runtime?.submittedPromptHash !== undefined
|
|
43
|
+
? { runtime: { submittedPromptHash: null } }
|
|
44
|
+
: {}),
|
|
45
|
+
}
|
|
41
46
|
: sessionMeta.browser;
|
|
42
47
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
43
48
|
status: "running",
|
|
44
49
|
startedAt: new Date().toISOString(),
|
|
45
50
|
mode,
|
|
46
|
-
...(browserConfig ? { browser:
|
|
51
|
+
...(browserConfig ? { browser: currentBrowser } : {}),
|
|
47
52
|
});
|
|
48
53
|
const notificationSettings = notifications ?? deriveNotificationSettingsFromMetadata(sessionMeta, process.env);
|
|
49
54
|
const modelForStatus = runOptions.model ?? sessionMeta.model;
|
|
55
|
+
const checkBrowserCancellation = () => {
|
|
56
|
+
if (mode === "browser" && signal?.aborted)
|
|
57
|
+
throw new BrowserRunCancelledError();
|
|
58
|
+
};
|
|
59
|
+
const finalizeBrowserCancellation = async (cancellationError) => {
|
|
60
|
+
const completedAt = new Date().toISOString();
|
|
61
|
+
log("Browser run cancelled.");
|
|
62
|
+
if (modelForStatus) {
|
|
63
|
+
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
64
|
+
status: "cancelled",
|
|
65
|
+
completedAt,
|
|
66
|
+
response: { status: "cancelled" },
|
|
67
|
+
error: undefined,
|
|
68
|
+
transport: undefined,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
await sessionStore.updateSession(sessionMeta.id, {
|
|
72
|
+
status: "cancelled",
|
|
73
|
+
completedAt,
|
|
74
|
+
errorMessage: undefined,
|
|
75
|
+
mode,
|
|
76
|
+
browser: browserConfig
|
|
77
|
+
? {
|
|
78
|
+
...currentBrowser,
|
|
79
|
+
config: browserConfig,
|
|
80
|
+
}
|
|
81
|
+
: undefined,
|
|
82
|
+
response: { status: "cancelled" },
|
|
83
|
+
error: undefined,
|
|
84
|
+
transport: undefined,
|
|
85
|
+
});
|
|
86
|
+
await retireCancelledBrowserTarget(sessionMeta.id, recoveryCaptureFromRuntime(currentBrowser?.runtime), log);
|
|
87
|
+
throw cancellationError;
|
|
88
|
+
};
|
|
50
89
|
try {
|
|
51
90
|
if (mode === "browser") {
|
|
52
91
|
if (!browserConfig) {
|
|
@@ -80,13 +119,17 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
80
119
|
browserConfig,
|
|
81
120
|
cwd,
|
|
82
121
|
log,
|
|
122
|
+
signal,
|
|
83
123
|
}, runnerDeps);
|
|
124
|
+
checkBrowserCancellation();
|
|
84
125
|
const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
|
|
126
|
+
checkBrowserCancellation();
|
|
85
127
|
const outputArtifacts = await copyBrowserOutputArtifacts({
|
|
86
128
|
outputPath: writtenOutputPath,
|
|
87
129
|
savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
|
|
88
130
|
log,
|
|
89
131
|
});
|
|
132
|
+
checkBrowserCancellation();
|
|
90
133
|
const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
|
|
91
134
|
await sendSessionNotification({
|
|
92
135
|
sessionId: sessionMeta.id,
|
|
@@ -96,13 +139,16 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
96
139
|
usage: result.usage,
|
|
97
140
|
characters: result.answerText?.length,
|
|
98
141
|
}, notificationSettings, log, result.answerText?.slice(0, 140));
|
|
142
|
+
checkBrowserCancellation();
|
|
99
143
|
if (modelForStatus) {
|
|
100
144
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
101
145
|
status: "completed",
|
|
102
146
|
completedAt: new Date().toISOString(),
|
|
103
147
|
usage: result.usage,
|
|
104
148
|
});
|
|
149
|
+
checkBrowserCancellation();
|
|
105
150
|
}
|
|
151
|
+
checkBrowserCancellation();
|
|
106
152
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
107
153
|
status: "completed",
|
|
108
154
|
completedAt: new Date().toISOString(),
|
|
@@ -401,6 +447,9 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
401
447
|
});
|
|
402
448
|
}
|
|
403
449
|
catch (error) {
|
|
450
|
+
if (error instanceof BrowserRunCancelledError) {
|
|
451
|
+
return await finalizeBrowserCancellation(error);
|
|
452
|
+
}
|
|
404
453
|
const message = formatError(error);
|
|
405
454
|
log(`ERROR: ${message}`);
|
|
406
455
|
markErrorLogged(error);
|
|
@@ -426,6 +475,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
426
475
|
const runtime = userError.details
|
|
427
476
|
?.runtime;
|
|
428
477
|
const recoverableRuntime = runtime ?? currentBrowser?.runtime;
|
|
478
|
+
currentBrowser = {
|
|
479
|
+
...currentBrowser,
|
|
480
|
+
config: browserConfig,
|
|
481
|
+
runtime: recoverableRuntime,
|
|
482
|
+
};
|
|
429
483
|
if (!hasRecoverableChatGptConversation(recoverableRuntime) &&
|
|
430
484
|
recoverableRuntime?.promptSubmitted !== true) {
|
|
431
485
|
log(dim("Chrome disconnected before a ChatGPT conversation was created; marking session error."));
|
|
@@ -495,22 +549,32 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
495
549
|
const connectionLostIntervalMs = configuredIntervalMs > 0
|
|
496
550
|
? configuredIntervalMs
|
|
497
551
|
: Math.max(1_000, Math.min(browserConfig?.timeoutMs ?? 30_000, 30_000));
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
552
|
+
let success;
|
|
553
|
+
try {
|
|
554
|
+
success = await autoReattachUntilComplete({
|
|
555
|
+
sessionMeta,
|
|
556
|
+
runtime: recoverableRuntime ?? undefined,
|
|
557
|
+
browserConfig: {
|
|
558
|
+
...browserConfig,
|
|
559
|
+
autoReattachIntervalMs: connectionLostIntervalMs,
|
|
560
|
+
autoReattachDelayMs: browserConfig?.autoReattachDelayMs ?? 0,
|
|
561
|
+
autoReattachTimeoutMs: browserConfig?.autoReattachTimeoutMs ?? browserConfig?.timeoutMs ?? 120_000,
|
|
562
|
+
},
|
|
563
|
+
browserMetadata: currentBrowser,
|
|
564
|
+
runOptions,
|
|
565
|
+
modelForStatus,
|
|
566
|
+
notificationSettings,
|
|
567
|
+
log,
|
|
568
|
+
maxAttempts: configuredIntervalMs > 0 ? undefined : 1,
|
|
569
|
+
signal,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
catch (recoveryError) {
|
|
573
|
+
if (signal?.aborted || recoveryError instanceof BrowserRunCancelledError) {
|
|
574
|
+
return await finalizeBrowserCancellation(new BrowserRunCancelledError());
|
|
575
|
+
}
|
|
576
|
+
throw recoveryError;
|
|
577
|
+
}
|
|
514
578
|
if (success) {
|
|
515
579
|
return;
|
|
516
580
|
}
|
|
@@ -531,6 +595,11 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
531
595
|
};
|
|
532
596
|
const autoReattachIntervalMs = browserConfig?.autoReattachIntervalMs ?? 0;
|
|
533
597
|
const autoRuntime = runtime ?? currentBrowser?.runtime;
|
|
598
|
+
currentBrowser = {
|
|
599
|
+
...currentBrowser,
|
|
600
|
+
config: browserConfig,
|
|
601
|
+
runtime: autoRuntime,
|
|
602
|
+
};
|
|
534
603
|
const willAutoReattach = autoReattachIntervalMs > 0 && Boolean(autoRuntime);
|
|
535
604
|
if (willAutoReattach) {
|
|
536
605
|
if (modelForStatus) {
|
|
@@ -554,16 +623,26 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
|
|
|
554
623
|
response: timeoutResponse,
|
|
555
624
|
error: timeoutError,
|
|
556
625
|
});
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
626
|
+
let success;
|
|
627
|
+
try {
|
|
628
|
+
success = await autoReattachUntilComplete({
|
|
629
|
+
sessionMeta,
|
|
630
|
+
runtime: autoRuntime,
|
|
631
|
+
browserConfig,
|
|
632
|
+
browserMetadata: currentBrowser,
|
|
633
|
+
runOptions,
|
|
634
|
+
modelForStatus,
|
|
635
|
+
notificationSettings,
|
|
636
|
+
log,
|
|
637
|
+
signal,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
catch (recoveryError) {
|
|
641
|
+
if (signal?.aborted || recoveryError instanceof BrowserRunCancelledError) {
|
|
642
|
+
return await finalizeBrowserCancellation(new BrowserRunCancelledError());
|
|
643
|
+
}
|
|
644
|
+
throw recoveryError;
|
|
645
|
+
}
|
|
567
646
|
if (success) {
|
|
568
647
|
return;
|
|
569
648
|
}
|
|
@@ -976,7 +1055,7 @@ async function writeAssistantOutput(targetPath, content, log) {
|
|
|
976
1055
|
log(dim(`write-output failed (${reason}); session completed anyway.`));
|
|
977
1056
|
}
|
|
978
1057
|
}
|
|
979
|
-
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, }) {
|
|
1058
|
+
async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig, browserMetadata, runOptions, modelForStatus, notificationSettings, log, maxAttempts, signal, }) {
|
|
980
1059
|
if (!runtime || !browserConfig) {
|
|
981
1060
|
log(dim("Auto-reattach disabled: missing runtime or browser config."));
|
|
982
1061
|
return false;
|
|
@@ -991,12 +1070,16 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
991
1070
|
120_000;
|
|
992
1071
|
const maxTotalMs = 2 * 60 * 60 * 1000; // 2h hard cap; avoid infinite polling by default.
|
|
993
1072
|
const maxDeadline = Date.now() + maxTotalMs;
|
|
1073
|
+
const checkCancellation = () => {
|
|
1074
|
+
if (signal?.aborted)
|
|
1075
|
+
throw new BrowserRunCancelledError();
|
|
1076
|
+
};
|
|
994
1077
|
const attemptLimit = typeof maxAttempts === "number" && maxAttempts > 0
|
|
995
1078
|
? Math.floor(maxAttempts)
|
|
996
1079
|
: Number.POSITIVE_INFINITY;
|
|
997
1080
|
if (delayMs > 0) {
|
|
998
1081
|
log(dim(`Auto-reattach starting in ${formatElapsed(delayMs)}...`));
|
|
999
|
-
await
|
|
1082
|
+
await delay(delayMs, signal);
|
|
1000
1083
|
}
|
|
1001
1084
|
if (Number.isFinite(attemptLimit)) {
|
|
1002
1085
|
log(dim(`Auto-reattach will try up to ${attemptLimit} attempt(s).`));
|
|
@@ -1012,6 +1095,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1012
1095
|
logger.verbose = true;
|
|
1013
1096
|
let attempt = 0;
|
|
1014
1097
|
for (;;) {
|
|
1098
|
+
checkCancellation();
|
|
1015
1099
|
const remainingBudgetMs = maxDeadline - Date.now();
|
|
1016
1100
|
if (remainingBudgetMs <= 0) {
|
|
1017
1101
|
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
@@ -1025,9 +1109,11 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1025
1109
|
...browserConfig,
|
|
1026
1110
|
timeoutMs,
|
|
1027
1111
|
};
|
|
1028
|
-
const result = await resumeBrowserSession(runtime, reattachConfig, logger, {
|
|
1112
|
+
const result = await awaitBrowserCancellation(resumeBrowserSession(runtime, reattachConfig, logger, {
|
|
1029
1113
|
promptPreview: sessionMeta.promptPreview,
|
|
1030
|
-
|
|
1114
|
+
signal,
|
|
1115
|
+
}), signal);
|
|
1116
|
+
checkCancellation();
|
|
1031
1117
|
captureSucceeded = true;
|
|
1032
1118
|
const answerText = result.answerMarkdown || result.answerText || "";
|
|
1033
1119
|
const outputTokens = estimateTokenCount(answerText);
|
|
@@ -1040,8 +1126,11 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1040
1126
|
existingArtifacts: sessionMeta.artifacts,
|
|
1041
1127
|
logger,
|
|
1042
1128
|
});
|
|
1129
|
+
checkCancellation();
|
|
1043
1130
|
const paths = await sessionStore.getPaths(sessionMeta.id);
|
|
1131
|
+
checkCancellation();
|
|
1044
1132
|
await fs.appendFile(paths.log, `[auto-reattach] captured assistant response on attempt ${attempt}\nAnswer:\n${answerText}\n`, "utf8");
|
|
1133
|
+
checkCancellation();
|
|
1045
1134
|
if (modelForStatus) {
|
|
1046
1135
|
await sessionStore.updateModelRun(sessionMeta.id, modelForStatus, {
|
|
1047
1136
|
status: "completed",
|
|
@@ -1053,8 +1142,10 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1053
1142
|
totalTokens: outputTokens,
|
|
1054
1143
|
},
|
|
1055
1144
|
});
|
|
1145
|
+
checkCancellation();
|
|
1056
1146
|
}
|
|
1057
1147
|
await writeAssistantOutput(runOptions.writeOutputPath, answerText, log);
|
|
1148
|
+
checkCancellation();
|
|
1058
1149
|
await sendSessionNotification({
|
|
1059
1150
|
sessionId: sessionMeta.id,
|
|
1060
1151
|
sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
|
|
@@ -1066,6 +1157,7 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1066
1157
|
},
|
|
1067
1158
|
characters: answerText.length,
|
|
1068
1159
|
}, notificationSettings, log, answerText.slice(0, 140));
|
|
1160
|
+
checkCancellation();
|
|
1069
1161
|
await sessionStore.updateSession(sessionMeta.id, {
|
|
1070
1162
|
status: "completed",
|
|
1071
1163
|
completedAt: new Date().toISOString(),
|
|
@@ -1091,6 +1183,9 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1091
1183
|
return true;
|
|
1092
1184
|
}
|
|
1093
1185
|
catch (error) {
|
|
1186
|
+
if (signal?.aborted || error instanceof BrowserRunCancelledError) {
|
|
1187
|
+
throw new BrowserRunCancelledError();
|
|
1188
|
+
}
|
|
1094
1189
|
if (captureSucceeded) {
|
|
1095
1190
|
const message = formatError(error);
|
|
1096
1191
|
if (modelForStatus) {
|
|
@@ -1128,7 +1223,25 @@ async function autoReattachUntilComplete({ sessionMeta, runtime, browserConfig,
|
|
|
1128
1223
|
log(dim(`Auto-reattach stopped after ${formatElapsed(maxTotalMs)} without capturing an answer.`));
|
|
1129
1224
|
return false;
|
|
1130
1225
|
}
|
|
1131
|
-
await
|
|
1226
|
+
await delay(Math.min(intervalMs, remainingAfterAttemptMs), signal);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
async function awaitBrowserCancellation(task, signal) {
|
|
1230
|
+
if (!signal)
|
|
1231
|
+
return task;
|
|
1232
|
+
if (signal.aborted)
|
|
1233
|
+
throw new BrowserRunCancelledError();
|
|
1234
|
+
let abort;
|
|
1235
|
+
const cancelled = new Promise((_, reject) => {
|
|
1236
|
+
abort = () => reject(new BrowserRunCancelledError());
|
|
1237
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1238
|
+
});
|
|
1239
|
+
try {
|
|
1240
|
+
return await Promise.race([task, cancelled]);
|
|
1241
|
+
}
|
|
1242
|
+
finally {
|
|
1243
|
+
if (abort)
|
|
1244
|
+
signal.removeEventListener("abort", abort);
|
|
1132
1245
|
}
|
|
1133
1246
|
}
|
|
1134
1247
|
export function deriveModelOutputPath(basePath, model) {
|
|
@@ -819,6 +819,7 @@ function sanitizeResult(result, warnings = []) {
|
|
|
819
819
|
tabUrl: result.tabUrl,
|
|
820
820
|
conversationId: result.conversationId,
|
|
821
821
|
promptSubmitted: result.promptSubmitted,
|
|
822
|
+
submittedPromptHash: result.submittedPromptHash,
|
|
822
823
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
823
824
|
chromePid: undefined,
|
|
824
825
|
chromePort: undefined,
|
|
@@ -304,7 +304,14 @@ export async function initializeSession(options, cwd, notifications, baseSlugOve
|
|
|
304
304
|
})),
|
|
305
305
|
cwd,
|
|
306
306
|
mode,
|
|
307
|
-
browser:
|
|
307
|
+
browser: mode === "browser"
|
|
308
|
+
? {
|
|
309
|
+
...(browserConfig ? { config: browserConfig } : {}),
|
|
310
|
+
runtime: { submittedPromptHash: null },
|
|
311
|
+
}
|
|
312
|
+
: browserConfig
|
|
313
|
+
? { config: browserConfig }
|
|
314
|
+
: undefined,
|
|
308
315
|
notifications,
|
|
309
316
|
options: {
|
|
310
317
|
prompt: options.prompt,
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steipete/oracle",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "CLI wrapper around OpenAI Responses API with GPT-5.6 Sol, GPT-5.6, GPT-5.5 Pro, GPT-5.5, GPT-5.4, GPT-5.2, GPT-5.1, and GPT-5.1 Codex high reasoning modes.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://askoracle.sh",
|
|
@@ -70,17 +70,17 @@
|
|
|
70
70
|
"dotenv": "^17.4.2",
|
|
71
71
|
"fast-glob": "^3.3.3",
|
|
72
72
|
"gpt-tokenizer": "^4.0.0",
|
|
73
|
-
"inquirer": "14.2.
|
|
73
|
+
"inquirer": "14.2.2",
|
|
74
74
|
"json5": "^2.2.3",
|
|
75
75
|
"kleur": "^4.1.5",
|
|
76
76
|
"markdansi": "0.3.3",
|
|
77
|
-
"openai": "^7.
|
|
77
|
+
"openai": "^7.13.0",
|
|
78
78
|
"osc-progress": "^0.3.3",
|
|
79
79
|
"qs": "^6.16.0",
|
|
80
80
|
"shiki": "^4.4.3",
|
|
81
81
|
"toasted-notifier": "^10.1.0",
|
|
82
|
-
"tokentally": "^0.1.
|
|
83
|
-
"zod": "^4.
|
|
82
|
+
"tokentally": "^0.1.6",
|
|
83
|
+
"zod": "^4.6.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@anthropic-ai/tokenizer": "^0.0.4",
|
|
@@ -88,13 +88,13 @@
|
|
|
88
88
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
89
89
|
"@types/chrome-remote-interface": "^0.34.0",
|
|
90
90
|
"@types/inquirer": "^9.0.10",
|
|
91
|
-
"@types/node": "^26.
|
|
91
|
+
"@types/node": "^26.5.1",
|
|
92
92
|
"@vitest/coverage-v8": "5.0.0",
|
|
93
|
-
"devtools-protocol": "0.0.
|
|
93
|
+
"devtools-protocol": "0.0.1694333",
|
|
94
94
|
"es-toolkit": "^1.52.0",
|
|
95
95
|
"esbuild": "^0.28.2",
|
|
96
|
-
"oxfmt": "0.
|
|
97
|
-
"oxlint": "^1.
|
|
96
|
+
"oxfmt": "0.67.0",
|
|
97
|
+
"oxlint": "^1.82.0",
|
|
98
98
|
"puppeteer-core": "^25.10.0",
|
|
99
99
|
"tsx": "^4.23.13",
|
|
100
100
|
"typescript": "^7.0.2",
|
|
Binary file
|
|
Binary file
|