@steipete/oracle 0.19.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 +72 -29
- package/dist/src/browser/actions/deepResearch.js +168 -44
- package/dist/src/browser/actions/modelSelection.js +78 -1
- package/dist/src/browser/actions/promptComposer.js +3 -0
- package/dist/src/browser/actions/thinkingTime.js +49 -6
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/chromeLifecycle.js +28 -16
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +144 -49
- package/dist/src/browser/liveTabs.js +11 -4
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +178 -109
- package/dist/src/browser/recoveryTarget.js +156 -0
- package/dist/src/browser/sessionRunner.js +33 -8
- package/dist/src/browser/tabLeaseRegistry.js +20 -0
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/cli/browserConfig.js +33 -3
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +38 -3
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/sessionDisplay.js +15 -8
- package/dist/src/cli/sessionRunner.js +153 -41
- package/dist/src/mcp/tools/consult.js +2 -2
- package/dist/src/mcp/types.js +1 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +3 -0
- package/dist/src/remote/server.js +2 -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 +10 -10
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -4,6 +4,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
4
4
|
import { lstat, mkdir, readFile, realpath, rename, rm, rmdir, stat, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { isProcessAlive, readProcessStartTimeMs } from "./profileState.js";
|
|
6
6
|
import { delay } from "./utils.js";
|
|
7
|
+
import { normalizeChromeHost } from "./targetClaim.js";
|
|
7
8
|
export const DEFAULT_MAX_CONCURRENT_CHATGPT_TABS = 3;
|
|
8
9
|
const REGISTRY_FILENAME = "oracle-tab-leases.json";
|
|
9
10
|
const REGISTRY_LOCK_DIRNAME = "oracle-tab-leases.lock";
|
|
@@ -187,6 +188,25 @@ export async function hasOtherActiveBrowserTabLeases(profileDir, leaseId, option
|
|
|
187
188
|
return active.some((lease) => lease.id !== leaseId);
|
|
188
189
|
});
|
|
189
190
|
}
|
|
191
|
+
export async function hasActiveBrowserTargetLease(profileDir, target) {
|
|
192
|
+
let raw;
|
|
193
|
+
try {
|
|
194
|
+
raw = await readFile(path.join(profileDir, REGISTRY_FILENAME), "utf8");
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === "ENOENT")
|
|
198
|
+
return false;
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
const registry = JSON.parse(raw);
|
|
202
|
+
if (registry.version !== 1 || !Array.isArray(registry.leases))
|
|
203
|
+
throw new Error("Unknown browser lease registry");
|
|
204
|
+
return registry.leases.some((lease) => lease.chromeTargetId === target.targetId &&
|
|
205
|
+
(!lease.chromeHost ||
|
|
206
|
+
normalizeChromeHost(lease.chromeHost) === normalizeChromeHost(target.host)) &&
|
|
207
|
+
(!lease.chromePort || lease.chromePort === target.port) &&
|
|
208
|
+
isProcessAlive(lease.pid));
|
|
209
|
+
}
|
|
190
210
|
async function withRegistryLock(profileDir, callback, signal) {
|
|
191
211
|
const lockDir = path.join(profileDir, REGISTRY_LOCK_DIRNAME);
|
|
192
212
|
const lockId = randomUUID();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { STOP_BUTTON_SELECTORS } from "./constants.js";
|
|
3
|
+
export function normalizeChromeHost(host) {
|
|
4
|
+
const value = host
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/^\[|\]$/g, "");
|
|
8
|
+
return value === "localhost" || value === "::1" || (isIP(value) === 4 && value.startsWith("127."))
|
|
9
|
+
? "loopback"
|
|
10
|
+
: value;
|
|
11
|
+
}
|
|
12
|
+
export function buildTargetClaimExpression(claimId) {
|
|
13
|
+
return `(() => {
|
|
14
|
+
if (window !== window.top) return true;
|
|
15
|
+
try {
|
|
16
|
+
const claim = JSON.parse(sessionStorage.getItem('oracle:target-claim') || 'null');
|
|
17
|
+
if (claim?.retiring) return false;
|
|
18
|
+
sessionStorage.setItem('oracle:target-claim', JSON.stringify({ id: ${JSON.stringify(claimId)}, retiring: false }));
|
|
19
|
+
return true;
|
|
20
|
+
} catch { return false; }
|
|
21
|
+
})()`;
|
|
22
|
+
}
|
|
23
|
+
export function buildTargetRetirementExpression(claimId, conversationId, reservationId, options = {}) {
|
|
24
|
+
return `(() => {
|
|
25
|
+
let claim;
|
|
26
|
+
try { claim = JSON.parse(sessionStorage.getItem('oracle:target-claim') || 'null'); } catch { return false; }
|
|
27
|
+
if (!claim || claim.id !== ${JSON.stringify(claimId)} || claim.retiring) return false;
|
|
28
|
+
const id = location.pathname.match(/\\/c\\/([^/]+)/)?.[1];
|
|
29
|
+
if (id !== ${JSON.stringify(conversationId)}) return false;
|
|
30
|
+
const generating = Array.from(document.querySelectorAll(${JSON.stringify(STOP_BUTTON_SELECTORS.join(","))})).some(node => node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0);
|
|
31
|
+
if (generating && !${JSON.stringify(options.allowGenerating === true)}) return false;
|
|
32
|
+
claim.retiring = true;
|
|
33
|
+
claim.retirementId = ${JSON.stringify(reservationId)};
|
|
34
|
+
try { sessionStorage.setItem('oracle:target-claim', JSON.stringify(claim)); } catch { return false; }
|
|
35
|
+
return true;
|
|
36
|
+
})()`;
|
|
37
|
+
}
|
|
38
|
+
export function buildTargetRetirementRollbackExpression(claimId, reservationId) {
|
|
39
|
+
return `(() => { try {
|
|
40
|
+
const claim = JSON.parse(sessionStorage.getItem('oracle:target-claim') || 'null');
|
|
41
|
+
if (claim?.id === ${JSON.stringify(claimId)} && claim.retirementId === ${JSON.stringify(reservationId)}) {
|
|
42
|
+
claim.retiring = false;
|
|
43
|
+
delete claim.retirementId;
|
|
44
|
+
sessionStorage.setItem('oracle:target-claim', JSON.stringify(claim));
|
|
45
|
+
}
|
|
46
|
+
} catch {} })()`;
|
|
47
|
+
}
|
|
48
|
+
/** Per-tab storage survives document replacement and serializes acquisition with retirement. */
|
|
49
|
+
export async function claimBrowserTarget(runtime, claimId) {
|
|
50
|
+
const source = buildTargetClaimExpression(claimId);
|
|
51
|
+
const claim = await runtime.evaluate({ expression: source, returnByValue: true });
|
|
52
|
+
if (claim.exceptionDetails || claim.result?.value !== true)
|
|
53
|
+
throw new Error("This Oracle tab is being retired after recovery; choose another browser tab.");
|
|
54
|
+
}
|
|
@@ -24,6 +24,10 @@ const CURRENT_CHATGPT_PRO_ALIASES = new Set([
|
|
|
24
24
|
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
25
25
|
const BROWSER_MODEL_LABELS = [
|
|
26
26
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
27
|
+
// GPT-6 (Astra) has no entry of its own in the ChatGPT picker: it is the "Latest" radio of the
|
|
28
|
+
// advanced view, and "GPT-6 Pro" is that radio with the power slider at Pro (composer pill "6 Pro").
|
|
29
|
+
["gpt-6-pro", "Latest"],
|
|
30
|
+
["gpt-6-astra", "Latest"],
|
|
27
31
|
["gpt-5.6-sol", "GPT-5.6 Sol"],
|
|
28
32
|
["gpt-5.6", "GPT-5.6 Sol"],
|
|
29
33
|
["gpt-5.5-pro", "GPT-5.5"],
|
|
@@ -47,6 +51,13 @@ const BROWSER_MODEL_LABELS = [
|
|
|
47
51
|
];
|
|
48
52
|
export function normalizeChatGptModelForBrowser(model) {
|
|
49
53
|
const normalized = model.toLowerCase();
|
|
54
|
+
// Browser-only alias: gpt-6-pro keeps its name so the Pro tier default survives (label "Latest").
|
|
55
|
+
if (isGpt6ProAlias(normalized)) {
|
|
56
|
+
return "gpt-6-pro";
|
|
57
|
+
}
|
|
58
|
+
if (isGpt6Alias(normalized)) {
|
|
59
|
+
return "gpt-6-astra";
|
|
60
|
+
}
|
|
50
61
|
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
51
62
|
return model;
|
|
52
63
|
}
|
|
@@ -72,6 +83,18 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
72
83
|
}
|
|
73
84
|
return model;
|
|
74
85
|
}
|
|
86
|
+
// Documented spellings only: gpt-6, gpt-6-astra, gpt-6-pro (plus their label forms such as
|
|
87
|
+
// "GPT-6 Pro") and "latest" map to ChatGPT's "Latest" model. Any other gpt-6-* id (gpt-6-codex,
|
|
88
|
+
// gpt-6-custom, ...) is not an alias and must pass through unchanged for custom/OpenRouter use.
|
|
89
|
+
const GPT6_ALIAS_PATTERN = /^gpt[-_ ]?6(?:[-_ ](?:astra|pro))?$/;
|
|
90
|
+
const GPT6_PRO_ALIAS_PATTERN = /^gpt[-_ ]?6[-_ ]pro$/;
|
|
91
|
+
export function isGpt6Alias(model) {
|
|
92
|
+
const normalized = model?.trim().toLowerCase() ?? "";
|
|
93
|
+
return normalized === "latest" || GPT6_ALIAS_PATTERN.test(normalized);
|
|
94
|
+
}
|
|
95
|
+
export function isGpt6ProAlias(model) {
|
|
96
|
+
return GPT6_PRO_ALIAS_PATTERN.test(model?.trim().toLowerCase() ?? "");
|
|
97
|
+
}
|
|
75
98
|
export function isCurrentChatGptProAlias(model) {
|
|
76
99
|
return CURRENT_CHATGPT_PRO_ALIASES.has(model?.trim().toLowerCase() ?? "");
|
|
77
100
|
}
|
|
@@ -80,7 +103,10 @@ export function resolveDefaultBrowserThinkingTime({ model, requestedModel, model
|
|
|
80
103
|
if (strategy !== "select")
|
|
81
104
|
return undefined;
|
|
82
105
|
const normalizedModel = normalizeChatGptModelForBrowser(model);
|
|
83
|
-
return isCurrentChatGptProAlias(requestedModel ?? model) ||
|
|
106
|
+
return isCurrentChatGptProAlias(requestedModel ?? model) ||
|
|
107
|
+
isGpt6ProAlias(requestedModel ?? model) ||
|
|
108
|
+
normalizedModel === "gpt-6-pro" ||
|
|
109
|
+
normalizedModel === "gpt-5.5-pro"
|
|
84
110
|
? "pro"
|
|
85
111
|
: undefined;
|
|
86
112
|
}
|
|
@@ -208,7 +234,9 @@ export async function buildBrowserConfig(options) {
|
|
|
208
234
|
remoteChrome,
|
|
209
235
|
browserTabRef: options.browserTab ?? undefined,
|
|
210
236
|
thinkingTime,
|
|
211
|
-
researchMode: options.browserResearch === "deep"
|
|
237
|
+
researchMode: options.browserResearch === "deep" || options.browserResearch === "search"
|
|
238
|
+
? options.browserResearch
|
|
239
|
+
: "off",
|
|
212
240
|
archiveConversations: options.browserArchive,
|
|
213
241
|
};
|
|
214
242
|
}
|
|
@@ -288,7 +316,9 @@ export function resolveBrowserModelLabel(input, model) {
|
|
|
288
316
|
return mapModelToBrowserLabel(model);
|
|
289
317
|
}
|
|
290
318
|
const normalizedInput = trimmed.toLowerCase();
|
|
291
|
-
if (normalizedInput === model.toLowerCase()
|
|
319
|
+
if (normalizedInput === model.toLowerCase() ||
|
|
320
|
+
(isGpt6Alias(normalizedInput) && (model === "gpt-6-astra" || model === "gpt-6-pro")) ||
|
|
321
|
+
(isGpt6ProAlias(normalizedInput) && model === "gpt-6-pro")) {
|
|
292
322
|
return mapModelToBrowserLabel(model);
|
|
293
323
|
}
|
|
294
324
|
return trimmed;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CHATGPT_URL } from "../browser/constants.js";
|
|
2
2
|
import { normalizeChatgptUrl } from "../browser/utils.js";
|
|
3
3
|
import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
|
|
4
|
+
import { isGpt6ProAlias } from "./browserConfig.js";
|
|
4
5
|
export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
5
6
|
const browser = config.browser;
|
|
6
7
|
if (!browser)
|
|
@@ -12,6 +13,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
12
13
|
const attachRunningRequested = options.browserAttachRunning === true ||
|
|
13
14
|
(isUnset("browserAttachRunning") && browser.attachRunning === true);
|
|
14
15
|
const currentModelRequestedByCli = options.browserModelStrategy === "current" && getSource("browserModelStrategy") === "cli";
|
|
16
|
+
const gpt6ProRequestedByCli = getSource("model") === "cli" && isGpt6ProAlias(options.model);
|
|
15
17
|
if (!options.copyProfile &&
|
|
16
18
|
isUnset("remoteChrome") &&
|
|
17
19
|
options.remoteChrome === undefined &&
|
|
@@ -104,6 +106,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
104
106
|
options.browserModelStrategy = browser.modelStrategy;
|
|
105
107
|
}
|
|
106
108
|
if (!currentModelRequestedByCli &&
|
|
109
|
+
!gpt6ProRequestedByCli &&
|
|
107
110
|
isUnset("browserThinkingTime") &&
|
|
108
111
|
browser.thinkingTime !== undefined) {
|
|
109
112
|
options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
|
|
@@ -2,12 +2,16 @@ 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";
|
|
8
10
|
import { persistBrowserHarvest } from "./harvestIntegrity.js";
|
|
11
|
+
import { completeOwnedBrowserHarvest } from "./recoveredBrowserHarvest.js";
|
|
9
12
|
const LIVE_POLL_MS = 2000;
|
|
10
13
|
const DEFAULT_STALL_THRESHOLD_MS = 60_000;
|
|
14
|
+
const HARVEST_FRESHNESS_POLL_MS = 250;
|
|
11
15
|
function isRecoverableMissingTabError(message) {
|
|
12
16
|
return (message.includes("No ChatGPT tab matched") ||
|
|
13
17
|
message.includes("No live ChatGPT tabs found") ||
|
|
@@ -30,6 +34,35 @@ function finishRecoveredChrome(recoveredChrome, closeAfterRecover) {
|
|
|
30
34
|
// best-effort cleanup
|
|
31
35
|
}
|
|
32
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
|
+
}
|
|
33
66
|
function sessionBrowserEndpoint(meta) {
|
|
34
67
|
const runtime = meta?.browser?.runtime ?? {};
|
|
35
68
|
const remote = meta?.browser?.config?.remoteChrome ?? {};
|
|
@@ -166,12 +199,12 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
166
199
|
try {
|
|
167
200
|
let harvested;
|
|
168
201
|
try {
|
|
169
|
-
harvested = await
|
|
202
|
+
harvested = await harvestSessionPrompt(meta, {
|
|
170
203
|
host: initialEndpoint.host,
|
|
171
204
|
port: initialEndpoint.port,
|
|
172
205
|
ref,
|
|
173
206
|
stallWindowMs: options.stallWindowMs,
|
|
174
|
-
});
|
|
207
|
+
}, !options.browserTabRef);
|
|
175
208
|
}
|
|
176
209
|
catch (error) {
|
|
177
210
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -183,7 +216,7 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
183
216
|
existingEndpoint: recordedEndpoint ?? undefined,
|
|
184
217
|
});
|
|
185
218
|
recoveredChrome = recovered.chrome;
|
|
186
|
-
harvested = await
|
|
219
|
+
harvested = await harvestSessionPrompt(meta, {
|
|
187
220
|
host: recovered.host,
|
|
188
221
|
port: recovered.port,
|
|
189
222
|
ref: recovered.ref,
|
|
@@ -199,6 +232,7 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
199
232
|
if (!options.quietOutput && output) {
|
|
200
233
|
process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
|
|
201
234
|
}
|
|
235
|
+
await completeOwnedBrowserHarvest(sessionId, harvested, integrity, (line) => console.log(line));
|
|
202
236
|
return harvested;
|
|
203
237
|
}
|
|
204
238
|
finally {
|
|
@@ -296,6 +330,7 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
|
|
|
296
330
|
if (output) {
|
|
297
331
|
process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
|
|
298
332
|
}
|
|
333
|
+
await completeOwnedBrowserHarvest(sessionId, finalHarvest, integrity, (line) => console.log(line));
|
|
299
334
|
return finalHarvest;
|
|
300
335
|
}
|
|
301
336
|
await new Promise((resolve) => setTimeout(resolve, LIVE_POLL_MS));
|
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));
|
package/dist/src/cli/options.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isGpt6Alias, isGpt6ProAlias } from "./browserConfig.js";
|
|
1
2
|
import { InvalidArgumentError } from "commander";
|
|
2
3
|
import { parseDuration } from "../duration.js";
|
|
3
4
|
import path from "node:path";
|
|
@@ -187,6 +188,12 @@ export function resolveApiModel(modelValue) {
|
|
|
187
188
|
if (normalized.includes("/")) {
|
|
188
189
|
return normalized;
|
|
189
190
|
}
|
|
191
|
+
if (isGpt6ProAlias(normalized)) {
|
|
192
|
+
throw new InvalidArgumentError("GPT-6 Pro is an API reasoning mode, not a model slug. Use --model gpt-6-astra --reasoning-mode pro (or --engine browser --model gpt-6-pro).");
|
|
193
|
+
}
|
|
194
|
+
if (isGpt6Alias(normalized)) {
|
|
195
|
+
return "gpt-6-astra";
|
|
196
|
+
}
|
|
190
197
|
const gpt56Label = parseBrowserGpt56Label(normalized);
|
|
191
198
|
if (gpt56Label?.variant.split(" ").includes("pro")) {
|
|
192
199
|
throw new InvalidArgumentError("GPT-5.6 Pro is an API reasoning mode, not a model slug. Use --model gpt-5.6-sol --reasoning-mode pro.");
|
|
@@ -284,6 +291,14 @@ export function inferModelFromLabel(modelValue) {
|
|
|
284
291
|
if (normalized.includes("/")) {
|
|
285
292
|
return normalized;
|
|
286
293
|
}
|
|
294
|
+
// gpt-6 / gpt-6-pro / latest: ChatGPT's "Latest" model (GPT-6 Astra). The browser-only -pro alias
|
|
295
|
+
// is passed through so its Pro tier default survives (resolveDefaultBrowserThinkingTime).
|
|
296
|
+
if (isGpt6ProAlias(normalized)) {
|
|
297
|
+
return "gpt-6-pro";
|
|
298
|
+
}
|
|
299
|
+
if (isGpt6Alias(normalized)) {
|
|
300
|
+
return "gpt-6-astra";
|
|
301
|
+
}
|
|
287
302
|
if (normalized.includes("grok")) {
|
|
288
303
|
return "grok-4.1";
|
|
289
304
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { sessionStore } from "../sessionStore.js";
|
|
3
|
+
import { isRecoveredConversationHarvestReady } from "../browser/recoverConversation.js";
|
|
4
|
+
import { hasOtherLiveBrowserController, matchesOwnedRecoveryTarget, retireRecoveredBrowserTarget, } from "../browser/recoveryTarget.js";
|
|
5
|
+
import { extractConversationIdFromUrl } from "../browser/reattachHelpers.js";
|
|
6
|
+
import { estimateTokenCount } from "../browser/utils.js";
|
|
7
|
+
export async function completeOwnedBrowserHarvest(sessionId, harvested, integrity, logger) {
|
|
8
|
+
if (harvested.state !== "completed" ||
|
|
9
|
+
harvested.stopExists ||
|
|
10
|
+
integrity.status !== "matched" ||
|
|
11
|
+
integrity.explicitTarget)
|
|
12
|
+
return;
|
|
13
|
+
const metadata = await sessionStore.readSession(sessionId);
|
|
14
|
+
if (!metadata?.browser?.runtime?.ownedRecoveryTarget ||
|
|
15
|
+
!isRecoveredConversationHarvestReady(harvested))
|
|
16
|
+
return;
|
|
17
|
+
const runtime = metadata.browser?.runtime;
|
|
18
|
+
const capture = {
|
|
19
|
+
host: harvested.host ?? "",
|
|
20
|
+
port: harvested.port ?? 0,
|
|
21
|
+
targetId: harvested.targetId,
|
|
22
|
+
browserWSEndpoint: runtime?.chromeBrowserWSEndpoint,
|
|
23
|
+
conversationId: harvested.conversationId ?? extractConversationIdFromUrl(harvested.url),
|
|
24
|
+
};
|
|
25
|
+
if (hasOtherLiveBrowserController(metadata) || !matchesOwnedRecoveryTarget(metadata, capture))
|
|
26
|
+
return;
|
|
27
|
+
// Harvest previously persisted only a snippet/hash. Save the full answer before retirement.
|
|
28
|
+
const answer = harvested.lastAssistantMarkdown || harvested.lastAssistantText;
|
|
29
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
30
|
+
await fs.appendFile(paths.log, `[reattach] harvested assistant response from existing Chrome tab\nAnswer:\n${answer}\n`, "utf8");
|
|
31
|
+
const outputTokens = estimateTokenCount(answer);
|
|
32
|
+
const usage = { inputTokens: 0, outputTokens, reasoningTokens: 0, totalTokens: outputTokens };
|
|
33
|
+
if (metadata.model) {
|
|
34
|
+
await sessionStore.updateModelRun(sessionId, metadata.model, {
|
|
35
|
+
status: "completed",
|
|
36
|
+
completedAt: new Date().toISOString(),
|
|
37
|
+
usage,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
await sessionStore.updateSession(sessionId, {
|
|
41
|
+
status: "completed",
|
|
42
|
+
completedAt: new Date().toISOString(),
|
|
43
|
+
usage,
|
|
44
|
+
errorMessage: undefined,
|
|
45
|
+
error: undefined,
|
|
46
|
+
transport: undefined,
|
|
47
|
+
response: { status: "completed" },
|
|
48
|
+
});
|
|
49
|
+
await retireRecoveredBrowserTarget(sessionId, capture, logger);
|
|
50
|
+
}
|
|
@@ -4,7 +4,7 @@ import { normalizeModelOption, inferModelFromLabel, resolveApiModel, normalizeBa
|
|
|
4
4
|
import { resolveGeminiModelId } from "../oracle/gemini.js";
|
|
5
5
|
import { resolveOverriddenApiModel } from "../oracle/modelResolver.js";
|
|
6
6
|
import { PromptValidationError } from "../oracle/errors.js";
|
|
7
|
-
import { normalizeChatGptModelForBrowser } from "./browserConfig.js";
|
|
7
|
+
import { normalizeChatGptModelForBrowser, isGpt6ProAlias } from "./browserConfig.js";
|
|
8
8
|
import { resolveConfiguredMaxFileSizeBytes } from "./fileSize.js";
|
|
9
9
|
import { isAzureOpenAICandidateModel } from "../oracle/providerRouting.js";
|
|
10
10
|
export function resolveRunOptionsFromConfig({ prompt, files = [], model, models, engine, userConfig, env = process.env, }) {
|
|
@@ -24,7 +24,10 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
24
24
|
.map((entry) => normalizeModelOption(entry))
|
|
25
25
|
.filter(Boolean);
|
|
26
26
|
const cliModelArg = normalizeModelOption(model ?? userConfig?.model) || DEFAULT_MODEL;
|
|
27
|
-
const
|
|
27
|
+
const isGpt6Pro = isGpt6ProAlias(cliModelArg);
|
|
28
|
+
const apiModel = isGpt6Pro && (resolvedEngine === "browser" || browserEngineRequested)
|
|
29
|
+
? "gpt-6-pro"
|
|
30
|
+
: resolveApiModel(cliModelArg);
|
|
28
31
|
// Browser label inference is intentionally engine-scoped: API model ids such as
|
|
29
32
|
// gpt-5.6-luna must remain provider values even though browser mode rejects
|
|
30
33
|
// unrecognized GPT-5.6 picker variants.
|
|
@@ -36,9 +39,11 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
36
39
|
const isGrok = apiModel.startsWith("grok");
|
|
37
40
|
const engineWasBrowser = resolvedEngine === "browser";
|
|
38
41
|
const allModels = normalizedRequestedModels.length > 0
|
|
39
|
-
? Array.from(new Set(normalizedRequestedModels.map((entry) =>
|
|
42
|
+
? Array.from(new Set(normalizedRequestedModels.map((entry) => isGpt6ProAlias(entry) && (resolvedEngine === "browser" || browserEngineRequested)
|
|
43
|
+
? "gpt-6-pro"
|
|
44
|
+
: resolveApiModel(entry))))
|
|
40
45
|
: [apiModel];
|
|
41
|
-
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel];
|
|
46
|
+
const browserCompatibilityModels = normalizedRequestedModels.length > 0 ? allModels : [browserModel ?? apiModel];
|
|
42
47
|
const isBrowserCompatible = (m) => m.startsWith("gpt-") || m.startsWith("gemini");
|
|
43
48
|
const hasNonBrowserCompatibleTarget = browserEngineRequested && browserCompatibilityModels.some((m) => !isBrowserCompatible(m));
|
|
44
49
|
if (hasNonBrowserCompatibleTarget) {
|
|
@@ -52,6 +57,16 @@ export function resolveRunOptionsFromConfig({ prompt, files = [], model, models,
|
|
|
52
57
|
const fixedEngine = isCodex || isClaude || isGrok || azureAutoApi || normalizedRequestedModels.length > 0
|
|
53
58
|
? "api"
|
|
54
59
|
: resolvedEngine;
|
|
60
|
+
if (fixedEngine === "api") {
|
|
61
|
+
if (isGpt6ProAlias(cliModelArg)) {
|
|
62
|
+
resolveApiModel(cliModelArg);
|
|
63
|
+
}
|
|
64
|
+
for (const entry of normalizedRequestedModels) {
|
|
65
|
+
if (isGpt6ProAlias(entry)) {
|
|
66
|
+
resolveApiModel(entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
55
70
|
// Browser runs use ChatGPT picker labels/aliases; API runs must keep API model ids intact.
|
|
56
71
|
const resolvedModel = fixedEngine === "browser" ? browserModel : apiModel;
|
|
57
72
|
const promptWithSuffix = userConfig?.promptSuffix && userConfig.promptSuffix.trim().length > 0
|
|
@@ -6,6 +6,7 @@ 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 { retireRecoveredBrowserTarget } from "../browser/recoveryTarget.js";
|
|
9
10
|
import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
|
|
10
11
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "../browser/artifacts.js";
|
|
11
12
|
import { estimateTokenCount } from "../browser/utils.js";
|
|
@@ -88,11 +89,8 @@ async function writeReattachAnswer(sessionId, result, replaceExistingLog) {
|
|
|
88
89
|
await fs.writeFile(paths.log, `[reattach] replaced incomplete Deep Research capture from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
logWriter.logLine("Answer:");
|
|
94
|
-
logWriter.logLine(body);
|
|
95
|
-
logWriter.stream.end();
|
|
92
|
+
const paths = await sessionStore.getPaths(sessionId);
|
|
93
|
+
await fs.appendFile(paths.log, `[reattach] captured assistant response from existing Chrome tab\nAnswer:\n${body}\n`, "utf8");
|
|
96
94
|
}
|
|
97
95
|
async function saveReattachBrowserArtifacts(sessionId, metadata, result) {
|
|
98
96
|
const body = result.answerMarkdown || result.answerText;
|
|
@@ -264,6 +262,7 @@ export async function attachSession(sessionId, options) {
|
|
|
264
262
|
transport: undefined,
|
|
265
263
|
});
|
|
266
264
|
console.log(chalk.green("Reattach succeeded; session marked completed."));
|
|
265
|
+
await retireRecoveredBrowserTarget(sessionId, result.captureTarget, (line) => console.log(dim(line)));
|
|
267
266
|
metadata = (await sessionStore.readSession(sessionId)) ?? metadata;
|
|
268
267
|
}
|
|
269
268
|
catch (error) {
|
|
@@ -360,7 +359,10 @@ export async function attachSession(sessionId, options) {
|
|
|
360
359
|
console.log(dim(`User error: ${userErrorSummary}`));
|
|
361
360
|
}
|
|
362
361
|
}
|
|
363
|
-
const shouldTrimIntro = initialStatus === "completed" ||
|
|
362
|
+
const shouldTrimIntro = initialStatus === "completed" ||
|
|
363
|
+
initialStatus === "partial" ||
|
|
364
|
+
initialStatus === "error" ||
|
|
365
|
+
initialStatus === "cancelled";
|
|
364
366
|
if (options?.renderPrompt !== false) {
|
|
365
367
|
const prompt = await readStoredPrompt(sessionId);
|
|
366
368
|
if (prompt) {
|
|
@@ -486,7 +488,10 @@ export async function attachSession(sessionId, options) {
|
|
|
486
488
|
if (!latest) {
|
|
487
489
|
break;
|
|
488
490
|
}
|
|
489
|
-
if (latest.status === "completed" ||
|
|
491
|
+
if (latest.status === "completed" ||
|
|
492
|
+
latest.status === "partial" ||
|
|
493
|
+
latest.status === "error" ||
|
|
494
|
+
latest.status === "cancelled") {
|
|
490
495
|
await printNew();
|
|
491
496
|
flushRemainder();
|
|
492
497
|
if (!options?.suppressMetadata) {
|
|
@@ -517,7 +522,9 @@ export async function attachSession(sessionId, options) {
|
|
|
517
522
|
if (!settled) {
|
|
518
523
|
break;
|
|
519
524
|
}
|
|
520
|
-
if (settled.status === "completed" ||
|
|
525
|
+
if (settled.status === "completed" ||
|
|
526
|
+
settled.status === "partial" ||
|
|
527
|
+
settled.status === "cancelled") {
|
|
521
528
|
continue;
|
|
522
529
|
}
|
|
523
530
|
await printNew();
|