pi-oracle 0.1.7 → 0.1.9
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.
|
@@ -106,7 +106,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
|
|
|
106
106
|
sessionPrefix: "oracle",
|
|
107
107
|
authSeedProfileDir: join(agentExtensionsDir, "oracle-auth-seed-profile"),
|
|
108
108
|
runtimeProfilesDir: join(agentExtensionsDir, "oracle-runtime-profiles"),
|
|
109
|
-
maxConcurrentJobs:
|
|
109
|
+
maxConcurrentJobs: 8,
|
|
110
110
|
cloneStrategy: "apfs-clone",
|
|
111
111
|
chatUrl: "https://chatgpt.com/",
|
|
112
112
|
authUrl: "https://chatgpt.com/auth/login",
|
|
@@ -7,6 +7,9 @@ import type { OracleConfig } from "./config.js";
|
|
|
7
7
|
import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withAuthLock } from "./locks.js";
|
|
8
8
|
|
|
9
9
|
const SEED_GENERATION_FILE = ".oracle-seed-generation";
|
|
10
|
+
const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
|
|
11
|
+
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
12
|
+
) || "agent-browser";
|
|
10
13
|
|
|
11
14
|
export interface OracleRuntimeLeaseMetadata {
|
|
12
15
|
jobId: string;
|
|
@@ -181,7 +184,7 @@ export interface OracleCleanupReport {
|
|
|
181
184
|
|
|
182
185
|
async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<string | undefined> {
|
|
183
186
|
return new Promise<string | undefined>((resolve) => {
|
|
184
|
-
const child = spawn(
|
|
187
|
+
const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { stdio: "ignore" });
|
|
185
188
|
let settled = false;
|
|
186
189
|
let timeout: NodeJS.Timeout | undefined;
|
|
187
190
|
let timedOut = false;
|
|
@@ -36,6 +36,9 @@ const REAL_CHROME_USER_DATA_DIR = resolve(homedir(), "Library", "Application Sup
|
|
|
36
36
|
const ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
37
37
|
const LOCKS_DIR = join(ORACLE_STATE_DIR, "locks");
|
|
38
38
|
const STALE_STAGING_PROFILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
39
|
+
const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
|
|
40
|
+
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
41
|
+
) || "agent-browser";
|
|
39
42
|
|
|
40
43
|
let runtimeProfileDir = config.browser.authSeedProfileDir;
|
|
41
44
|
|
|
@@ -165,7 +168,7 @@ function targetBrowserBaseArgs(options = {}) {
|
|
|
165
168
|
|
|
166
169
|
async function closeTargetBrowser() {
|
|
167
170
|
await log(`Closing target browser session ${authSessionName()} if present`);
|
|
168
|
-
const result = await spawnCommand(
|
|
171
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "close"], { allowFailure: true });
|
|
169
172
|
await log(`close result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
170
173
|
}
|
|
171
174
|
|
|
@@ -182,7 +185,7 @@ async function ensureNotSymlink(path, label) {
|
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
async function isAuthBrowserConnected() {
|
|
185
|
-
const result = await spawnCommand(
|
|
188
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
|
|
186
189
|
try {
|
|
187
190
|
const parsed = JSON.parse(result.stdout || "{}");
|
|
188
191
|
return parsed?.data?.connected === true;
|
|
@@ -274,7 +277,7 @@ async function launchTargetBrowser() {
|
|
|
274
277
|
await closeTargetBrowser();
|
|
275
278
|
const args = [...targetBrowserBaseArgs({ withLaunchOptions: true, mode: "headed" }), "open", "about:blank"];
|
|
276
279
|
await log(`Launching isolated browser: agent-browser ${JSON.stringify(args)}`);
|
|
277
|
-
const result = await spawnCommand(
|
|
280
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, args, { allowFailure: true });
|
|
278
281
|
await log(`launch result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
279
282
|
if (result.code !== 0) {
|
|
280
283
|
throw new Error(result.stderr || result.stdout || "Failed to launch isolated oracle browser");
|
|
@@ -282,7 +285,7 @@ async function launchTargetBrowser() {
|
|
|
282
285
|
}
|
|
283
286
|
|
|
284
287
|
async function streamStatus() {
|
|
285
|
-
const result = await spawnCommand(
|
|
288
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
|
|
286
289
|
await log(`stream status: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
287
290
|
try {
|
|
288
291
|
const parsed = JSON.parse(result.stdout || "{}");
|
|
@@ -311,7 +314,7 @@ async function targetCommand(...args) {
|
|
|
311
314
|
options = args.pop();
|
|
312
315
|
}
|
|
313
316
|
await ensureBrowserConnected();
|
|
314
|
-
const result = await spawnCommand(
|
|
317
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), ...args], options);
|
|
315
318
|
const label = options?.logLabel || `agent-browser ${args.join(" ")}`;
|
|
316
319
|
await log(`${label}: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
317
320
|
return result;
|
|
@@ -37,6 +37,9 @@ const ARTIFACT_DOWNLOAD_HEARTBEAT_MS = 10_000;
|
|
|
37
37
|
const ARTIFACT_DOWNLOAD_TIMEOUT_MS = 90_000;
|
|
38
38
|
const ARTIFACT_DOWNLOAD_MAX_ATTEMPTS = 2;
|
|
39
39
|
const AGENT_BROWSER_CLOSE_TIMEOUT_MS = 10_000;
|
|
40
|
+
const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
|
|
41
|
+
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
42
|
+
) || "agent-browser";
|
|
40
43
|
|
|
41
44
|
let currentJob;
|
|
42
45
|
let browserStarted = false;
|
|
@@ -321,7 +324,7 @@ async function closeBrowser(job) {
|
|
|
321
324
|
if (cleaningUpBrowser) return;
|
|
322
325
|
cleaningUpBrowser = true;
|
|
323
326
|
try {
|
|
324
|
-
const result = await spawnCommand(
|
|
327
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "close"], {
|
|
325
328
|
allowFailure: true,
|
|
326
329
|
timeoutMs: AGENT_BROWSER_CLOSE_TIMEOUT_MS,
|
|
327
330
|
});
|
|
@@ -337,12 +340,12 @@ async function closeBrowser(job) {
|
|
|
337
340
|
async function launchBrowser(job, url) {
|
|
338
341
|
await closeBrowser(job);
|
|
339
342
|
const mode = job.config.browser.runMode;
|
|
340
|
-
await spawnCommand(
|
|
343
|
+
await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job, { withLaunchOptions: true, mode }), "open", url]);
|
|
341
344
|
browserStarted = true;
|
|
342
345
|
}
|
|
343
346
|
|
|
344
347
|
async function streamStatus(job) {
|
|
345
|
-
const { stdout } = await spawnCommand(
|
|
348
|
+
const { stdout } = await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "--json", "stream", "status"], { allowFailure: true });
|
|
346
349
|
try {
|
|
347
350
|
const parsed = JSON.parse(stdout || "{}");
|
|
348
351
|
return parsed?.data || {};
|
|
@@ -374,7 +377,7 @@ async function agentBrowser(job, ...args) {
|
|
|
374
377
|
options = args.pop();
|
|
375
378
|
}
|
|
376
379
|
await ensureBrowserConnected(job);
|
|
377
|
-
return spawnCommand(
|
|
380
|
+
return spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), ...args], options);
|
|
378
381
|
}
|
|
379
382
|
|
|
380
383
|
function parseEvalResult(stdout) {
|
|
@@ -829,6 +832,17 @@ function detectUploadErrorText(text) {
|
|
|
829
832
|
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
830
833
|
}
|
|
831
834
|
|
|
835
|
+
function detectResponseFailureText(text) {
|
|
836
|
+
const patterns = [
|
|
837
|
+
"Message delivery timed out",
|
|
838
|
+
"A network error occurred",
|
|
839
|
+
"An error occurred while connecting to the websocket",
|
|
840
|
+
"There was an error generating a response",
|
|
841
|
+
"Something went wrong while generating the response",
|
|
842
|
+
];
|
|
843
|
+
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
844
|
+
}
|
|
845
|
+
|
|
832
846
|
function composerSnapshotSlice(snapshot) {
|
|
833
847
|
const lines = snapshot.split("\n");
|
|
834
848
|
let composerIndex = -1;
|
|
@@ -1120,17 +1134,39 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1120
1134
|
const timeoutAt = Date.now() + job.config.worker.completionTimeoutMs;
|
|
1121
1135
|
let lastText = "";
|
|
1122
1136
|
let stableCount = 0;
|
|
1137
|
+
let retriedAfterFailure = false;
|
|
1123
1138
|
|
|
1124
1139
|
while (Date.now() < timeoutAt) {
|
|
1125
1140
|
await heartbeat();
|
|
1126
|
-
const snapshot = await snapshotText(job);
|
|
1141
|
+
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1127
1142
|
const hasStopStreaming = snapshot.includes("Stop streaming");
|
|
1143
|
+
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1128
1144
|
const copyResponseCount = (snapshot.match(/Copy response/g) || []).length;
|
|
1145
|
+
const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
|
|
1129
1146
|
const messages = await assistantMessages(job);
|
|
1130
1147
|
const targetMessage = messages[baselineAssistantCount];
|
|
1131
1148
|
const targetText = targetMessage?.text || "";
|
|
1132
1149
|
const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount;
|
|
1133
1150
|
|
|
1151
|
+
if (!hasStopStreaming && hasRetryButton && responseFailureText) {
|
|
1152
|
+
if (!retriedAfterFailure) {
|
|
1153
|
+
const retryEntry = findEntry(
|
|
1154
|
+
snapshot,
|
|
1155
|
+
(candidate) => candidate.kind === "button" && candidate.label === "Retry" && !candidate.disabled,
|
|
1156
|
+
);
|
|
1157
|
+
if (retryEntry) {
|
|
1158
|
+
retriedAfterFailure = true;
|
|
1159
|
+
lastText = "";
|
|
1160
|
+
stableCount = 0;
|
|
1161
|
+
await log(`Response delivery failed (${responseFailureText}); clicking Retry once`);
|
|
1162
|
+
await clickRef(job, retryEntry.ref);
|
|
1163
|
+
await agentBrowser(job, "wait", "1000").catch(() => undefined);
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
throw new Error(`ChatGPT response failed: ${responseFailureText}`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1134
1170
|
if (!hasStopStreaming && hasTargetCopyResponse && targetText) {
|
|
1135
1171
|
if (targetText === lastText) stableCount += 1;
|
|
1136
1172
|
else stableCount = 1;
|