pi-oracle 0.1.8 → 0.1.10
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/extensions/oracle/lib/jobs.ts +18 -7
- package/extensions/oracle/lib/locks.ts +3 -1
- package/extensions/oracle/lib/runtime.ts +7 -2
- package/extensions/oracle/worker/artifact-heuristics.mjs +76 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +10 -6
- package/extensions/oracle/worker/run-job.mjs +209 -94
- package/package.json +3 -3
|
@@ -30,6 +30,9 @@ export const ORACLE_STALE_HEARTBEAT_MS = 3 * 60 * 1000;
|
|
|
30
30
|
export const ORACLE_NOTIFICATION_CLAIM_TTL_MS = 60_000;
|
|
31
31
|
const ORACLE_COMPLETE_JOB_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
32
32
|
const ORACLE_FAILED_JOB_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
33
|
+
export const DEFAULT_ORACLE_JOBS_DIR = "/tmp";
|
|
34
|
+
export const ORACLE_JOBS_DIR_ENV = "PI_ORACLE_JOBS_DIR";
|
|
35
|
+
const ORACLE_JOBS_DIR = process.env[ORACLE_JOBS_DIR_ENV]?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
33
36
|
|
|
34
37
|
export function isActiveOracleJob(job: Pick<OracleJob, "status">): boolean {
|
|
35
38
|
return ACTIVE_ORACLE_JOB_STATUSES.includes(job.status);
|
|
@@ -148,20 +151,24 @@ export function getSessionFile(ctx: ExtensionContext): string | undefined {
|
|
|
148
151
|
return manager.getSessionFile?.();
|
|
149
152
|
}
|
|
150
153
|
|
|
154
|
+
export function getOracleJobsDir(): string {
|
|
155
|
+
return ORACLE_JOBS_DIR;
|
|
156
|
+
}
|
|
157
|
+
|
|
151
158
|
export function getJobDir(id: string): string {
|
|
152
|
-
return join(
|
|
159
|
+
return join(ORACLE_JOBS_DIR, `oracle-${id}`);
|
|
153
160
|
}
|
|
154
161
|
|
|
155
162
|
export function listOracleJobDirs(): string[] {
|
|
156
|
-
if (!existsSync(
|
|
157
|
-
return readdirSync(
|
|
163
|
+
if (!existsSync(ORACLE_JOBS_DIR)) return [];
|
|
164
|
+
return readdirSync(ORACLE_JOBS_DIR)
|
|
158
165
|
.filter((name) => name.startsWith("oracle-"))
|
|
159
|
-
.map((name) => join(
|
|
166
|
+
.map((name) => join(ORACLE_JOBS_DIR, name))
|
|
160
167
|
.filter((path) => existsSync(join(path, "job.json")));
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
export function readJob(jobDirOrId: string): OracleJob | undefined {
|
|
164
|
-
const jobDir = jobDirOrId.
|
|
171
|
+
const jobDir = existsSync(join(jobDirOrId, "job.json")) ? jobDirOrId : getJobDir(jobDirOrId);
|
|
165
172
|
const jobPath = join(jobDir, "job.json");
|
|
166
173
|
if (!existsSync(jobPath)) return undefined;
|
|
167
174
|
try {
|
|
@@ -528,6 +535,10 @@ export async function createJob(
|
|
|
528
535
|
await chmod(promptPath, 0o600).catch(() => undefined);
|
|
529
536
|
|
|
530
537
|
const now = new Date().toISOString();
|
|
538
|
+
const normalizedEffort = input.modelFamily === "instant" ? undefined : (input.effort ?? config.defaults.effort);
|
|
539
|
+
const normalizedAutoSwitchToThinking = input.modelFamily === "instant"
|
|
540
|
+
? (input.autoSwitchToThinking ?? config.defaults.autoSwitchToThinking)
|
|
541
|
+
: false;
|
|
531
542
|
const job: OracleJob = {
|
|
532
543
|
id,
|
|
533
544
|
status: "submitted",
|
|
@@ -541,8 +552,8 @@ export async function createJob(
|
|
|
541
552
|
originSessionFile,
|
|
542
553
|
requestSource: input.requestSource,
|
|
543
554
|
chatModelFamily: input.modelFamily,
|
|
544
|
-
effort:
|
|
545
|
-
autoSwitchToThinking:
|
|
555
|
+
effort: normalizedEffort,
|
|
556
|
+
autoSwitchToThinking: normalizedAutoSwitchToThinking,
|
|
546
557
|
followUpToJobId: input.followUpToJobId,
|
|
547
558
|
chatUrl: input.followUpToJobId ? input.chatUrl : undefined,
|
|
548
559
|
conversationId,
|
|
@@ -4,7 +4,9 @@ import { mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
|
4
4
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
export const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
8
|
+
export const ORACLE_STATE_DIR_ENV = "PI_ORACLE_STATE_DIR";
|
|
9
|
+
const ORACLE_STATE_DIR = process.env[ORACLE_STATE_DIR_ENV]?.trim() || DEFAULT_ORACLE_STATE_DIR;
|
|
8
10
|
const LOCKS_DIR = join(ORACLE_STATE_DIR, "locks");
|
|
9
11
|
const LEASES_DIR = join(ORACLE_STATE_DIR, "leases");
|
|
10
12
|
const DEFAULT_WAIT_MS = 30_000;
|
|
@@ -7,6 +7,11 @@ 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 DEFAULT_ORACLE_JOBS_DIR = "/tmp";
|
|
11
|
+
const ORACLE_JOBS_DIR = process.env.PI_ORACLE_JOBS_DIR?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
12
|
+
const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
|
|
13
|
+
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
14
|
+
) || "agent-browser";
|
|
10
15
|
|
|
11
16
|
export interface OracleRuntimeLeaseMetadata {
|
|
12
17
|
jobId: string;
|
|
@@ -80,7 +85,7 @@ export async function writeSeedGeneration(config: OracleConfig, value = new Date
|
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
function activeJobExists(jobId: string): boolean {
|
|
83
|
-
const path = join(
|
|
88
|
+
const path = join(ORACLE_JOBS_DIR, `oracle-${jobId}`, "job.json");
|
|
84
89
|
if (!existsSync(path)) return false;
|
|
85
90
|
try {
|
|
86
91
|
const job = JSON.parse(readFileSync(path, "utf8")) as { status?: string };
|
|
@@ -181,7 +186,7 @@ export interface OracleCleanupReport {
|
|
|
181
186
|
|
|
182
187
|
async function closeRuntimeBrowserSession(runtimeSessionName: string): Promise<string | undefined> {
|
|
183
188
|
return new Promise<string | undefined>((resolve) => {
|
|
184
|
-
const child = spawn(
|
|
189
|
+
const child = spawn(AGENT_BROWSER_BIN, ["--session", runtimeSessionName, "close"], { stdio: "ignore" });
|
|
185
190
|
let settled = false;
|
|
186
191
|
let timeout: NodeJS.Timeout | undefined;
|
|
187
192
|
let timedOut = false;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const FILE_LABEL_PATTERN_SOURCE = String.raw`(?:^|[^\w])[^\n]*\.[A-Za-z0-9]{1,12}(?:$|[^\w])`;
|
|
2
|
+
const FILE_LABEL_PATTERN = new RegExp(FILE_LABEL_PATTERN_SOURCE);
|
|
3
|
+
export const GENERIC_ARTIFACT_LABELS = ["ATTACHED", "DONE"];
|
|
4
|
+
const GENERIC_ARTIFACT_LABEL_SET = new Set(GENERIC_ARTIFACT_LABELS);
|
|
5
|
+
|
|
6
|
+
export function parseSnapshotEntries(snapshot) {
|
|
7
|
+
return String(snapshot || "")
|
|
8
|
+
.split("\n")
|
|
9
|
+
.map((line, lineIndex) => {
|
|
10
|
+
const refMatch = line.match(/\bref=(e\d+)\b/);
|
|
11
|
+
if (!refMatch) return undefined;
|
|
12
|
+
const kindMatch = line.match(/^\s*-\s*([^\s]+)/);
|
|
13
|
+
const quotedMatch = line.match(/"([^"]*)"/);
|
|
14
|
+
const valueMatch = line.match(/:\s*(.+)$/);
|
|
15
|
+
return {
|
|
16
|
+
line,
|
|
17
|
+
lineIndex,
|
|
18
|
+
ref: `@${refMatch[1]}`,
|
|
19
|
+
kind: kindMatch ? kindMatch[1] : undefined,
|
|
20
|
+
label: quotedMatch ? quotedMatch[1] : undefined,
|
|
21
|
+
value: valueMatch ? valueMatch[1].trim() : undefined,
|
|
22
|
+
disabled: /\bdisabled\b/.test(line),
|
|
23
|
+
};
|
|
24
|
+
})
|
|
25
|
+
.filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeText(value) {
|
|
29
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isLikelyArtifactLabel(label) {
|
|
33
|
+
const normalized = normalizeText(label);
|
|
34
|
+
if (!normalized) return false;
|
|
35
|
+
if (GENERIC_ARTIFACT_LABEL_SET.has(normalized.toUpperCase())) return true;
|
|
36
|
+
return FILE_LABEL_PATTERN.test(normalized);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isStructuralArtifactCandidate(candidate) {
|
|
40
|
+
const label = normalizeText(candidate?.label);
|
|
41
|
+
if (!isLikelyArtifactLabel(label)) return false;
|
|
42
|
+
|
|
43
|
+
const listItemText = normalizeText(candidate?.listItemText);
|
|
44
|
+
const listItemFileButtonCount = Number(candidate?.listItemFileButtonCount || 0);
|
|
45
|
+
const paragraphFileButtonCount = Number(candidate?.paragraphFileButtonCount || 0);
|
|
46
|
+
const paragraphOtherTextLength = Number(candidate?.paragraphOtherTextLength ?? Number.POSITIVE_INFINITY);
|
|
47
|
+
const focusableFileButtonCount = Number(candidate?.focusableFileButtonCount || 0);
|
|
48
|
+
const focusableOtherTextLength = Number(candidate?.focusableOtherTextLength ?? Number.POSITIVE_INFINITY);
|
|
49
|
+
|
|
50
|
+
if (listItemText === label && listItemFileButtonCount === 1) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (paragraphFileButtonCount === 1 && paragraphOtherTextLength <= 32) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (focusableFileButtonCount >= 1 && focusableOtherTextLength <= 64) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function filterStructuralArtifactCandidates(candidates) {
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
const filtered = [];
|
|
68
|
+
for (const candidate of candidates || []) {
|
|
69
|
+
const label = normalizeText(candidate?.label);
|
|
70
|
+
if (!label || seen.has(label)) continue;
|
|
71
|
+
if (!isStructuralArtifactCandidate(candidate)) continue;
|
|
72
|
+
seen.add(label);
|
|
73
|
+
filtered.push({ label });
|
|
74
|
+
}
|
|
75
|
+
return filtered;
|
|
76
|
+
}
|
|
@@ -33,9 +33,13 @@ const SNAPSHOT_PATH = "/tmp/oracle-auth.snapshot.txt";
|
|
|
33
33
|
const BODY_PATH = "/tmp/oracle-auth.body.txt";
|
|
34
34
|
const SCREENSHOT_PATH = "/tmp/oracle-auth.png";
|
|
35
35
|
const REAL_CHROME_USER_DATA_DIR = resolve(homedir(), "Library", "Application Support", "Google", "Chrome");
|
|
36
|
-
const
|
|
36
|
+
const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
37
|
+
const ORACLE_STATE_DIR = process.env.PI_ORACLE_STATE_DIR?.trim() || DEFAULT_ORACLE_STATE_DIR;
|
|
37
38
|
const LOCKS_DIR = join(ORACLE_STATE_DIR, "locks");
|
|
38
39
|
const STALE_STAGING_PROFILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
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";
|
|
39
43
|
|
|
40
44
|
let runtimeProfileDir = config.browser.authSeedProfileDir;
|
|
41
45
|
|
|
@@ -165,7 +169,7 @@ function targetBrowserBaseArgs(options = {}) {
|
|
|
165
169
|
|
|
166
170
|
async function closeTargetBrowser() {
|
|
167
171
|
await log(`Closing target browser session ${authSessionName()} if present`);
|
|
168
|
-
const result = await spawnCommand(
|
|
172
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "close"], { allowFailure: true });
|
|
169
173
|
await log(`close result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
170
174
|
}
|
|
171
175
|
|
|
@@ -182,7 +186,7 @@ async function ensureNotSymlink(path, label) {
|
|
|
182
186
|
}
|
|
183
187
|
|
|
184
188
|
async function isAuthBrowserConnected() {
|
|
185
|
-
const result = await spawnCommand(
|
|
189
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
|
|
186
190
|
try {
|
|
187
191
|
const parsed = JSON.parse(result.stdout || "{}");
|
|
188
192
|
return parsed?.data?.connected === true;
|
|
@@ -274,7 +278,7 @@ async function launchTargetBrowser() {
|
|
|
274
278
|
await closeTargetBrowser();
|
|
275
279
|
const args = [...targetBrowserBaseArgs({ withLaunchOptions: true, mode: "headed" }), "open", "about:blank"];
|
|
276
280
|
await log(`Launching isolated browser: agent-browser ${JSON.stringify(args)}`);
|
|
277
|
-
const result = await spawnCommand(
|
|
281
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, args, { allowFailure: true });
|
|
278
282
|
await log(`launch result: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
279
283
|
if (result.code !== 0) {
|
|
280
284
|
throw new Error(result.stderr || result.stdout || "Failed to launch isolated oracle browser");
|
|
@@ -282,7 +286,7 @@ async function launchTargetBrowser() {
|
|
|
282
286
|
}
|
|
283
287
|
|
|
284
288
|
async function streamStatus() {
|
|
285
|
-
const result = await spawnCommand(
|
|
289
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), "--json", "stream", "status"], { allowFailure: true });
|
|
286
290
|
await log(`stream status: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
287
291
|
try {
|
|
288
292
|
const parsed = JSON.parse(result.stdout || "{}");
|
|
@@ -311,7 +315,7 @@ async function targetCommand(...args) {
|
|
|
311
315
|
options = args.pop();
|
|
312
316
|
}
|
|
313
317
|
await ensureBrowserConnected();
|
|
314
|
-
const result = await spawnCommand(
|
|
318
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...targetBrowserBaseArgs(), ...args], options);
|
|
315
319
|
const label = options?.logLabel || `agent-browser ${args.join(" ")}`;
|
|
316
320
|
await log(`${label}: code=${result.code} stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
|
|
317
321
|
return result;
|
|
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { appendFile, chmod, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
import { FILE_LABEL_PATTERN_SOURCE, filterStructuralArtifactCandidates, GENERIC_ARTIFACT_LABELS, parseSnapshotEntries } from "./artifact-heuristics.mjs";
|
|
6
7
|
|
|
7
8
|
const jobId = process.argv[2];
|
|
8
9
|
if (!jobId) {
|
|
@@ -10,7 +11,9 @@ if (!jobId) {
|
|
|
10
11
|
process.exit(1);
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const DEFAULT_ORACLE_JOBS_DIR = "/tmp";
|
|
15
|
+
const ORACLE_JOBS_DIR = process.env.PI_ORACLE_JOBS_DIR?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
16
|
+
const jobDir = join(ORACLE_JOBS_DIR, `oracle-${jobId}`);
|
|
14
17
|
const jobPath = `${jobDir}/job.json`;
|
|
15
18
|
const CHATGPT_LABELS = {
|
|
16
19
|
composer: "Chat with ChatGPT",
|
|
@@ -26,7 +29,8 @@ const MODEL_FAMILY_PREFIX = {
|
|
|
26
29
|
pro: "Pro ",
|
|
27
30
|
};
|
|
28
31
|
|
|
29
|
-
const
|
|
32
|
+
const DEFAULT_ORACLE_STATE_DIR = "/tmp/pi-oracle-state";
|
|
33
|
+
const ORACLE_STATE_DIR = process.env.PI_ORACLE_STATE_DIR?.trim() || DEFAULT_ORACLE_STATE_DIR;
|
|
30
34
|
const LOCKS_DIR = join(ORACLE_STATE_DIR, "locks");
|
|
31
35
|
const LEASES_DIR = join(ORACLE_STATE_DIR, "leases");
|
|
32
36
|
const SEED_GENERATION_FILE = ".oracle-seed-generation";
|
|
@@ -37,6 +41,12 @@ const ARTIFACT_DOWNLOAD_HEARTBEAT_MS = 10_000;
|
|
|
37
41
|
const ARTIFACT_DOWNLOAD_TIMEOUT_MS = 90_000;
|
|
38
42
|
const ARTIFACT_DOWNLOAD_MAX_ATTEMPTS = 2;
|
|
39
43
|
const AGENT_BROWSER_CLOSE_TIMEOUT_MS = 10_000;
|
|
44
|
+
const MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS = 20_000;
|
|
45
|
+
const MODEL_CONFIGURATION_SETTLE_POLL_MS = 250;
|
|
46
|
+
const MODEL_CONFIGURATION_CLOSE_RETRY_MS = 1_000;
|
|
47
|
+
const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
|
|
48
|
+
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
49
|
+
) || "agent-browser";
|
|
40
50
|
|
|
41
51
|
let currentJob;
|
|
42
52
|
let browserStarted = false;
|
|
@@ -321,7 +331,7 @@ async function closeBrowser(job) {
|
|
|
321
331
|
if (cleaningUpBrowser) return;
|
|
322
332
|
cleaningUpBrowser = true;
|
|
323
333
|
try {
|
|
324
|
-
const result = await spawnCommand(
|
|
334
|
+
const result = await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "close"], {
|
|
325
335
|
allowFailure: true,
|
|
326
336
|
timeoutMs: AGENT_BROWSER_CLOSE_TIMEOUT_MS,
|
|
327
337
|
});
|
|
@@ -337,12 +347,12 @@ async function closeBrowser(job) {
|
|
|
337
347
|
async function launchBrowser(job, url) {
|
|
338
348
|
await closeBrowser(job);
|
|
339
349
|
const mode = job.config.browser.runMode;
|
|
340
|
-
await spawnCommand(
|
|
350
|
+
await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job, { withLaunchOptions: true, mode }), "open", url]);
|
|
341
351
|
browserStarted = true;
|
|
342
352
|
}
|
|
343
353
|
|
|
344
354
|
async function streamStatus(job) {
|
|
345
|
-
const { stdout } = await spawnCommand(
|
|
355
|
+
const { stdout } = await spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), "--json", "stream", "status"], { allowFailure: true });
|
|
346
356
|
try {
|
|
347
357
|
const parsed = JSON.parse(stdout || "{}");
|
|
348
358
|
return parsed?.data || {};
|
|
@@ -374,7 +384,7 @@ async function agentBrowser(job, ...args) {
|
|
|
374
384
|
options = args.pop();
|
|
375
385
|
}
|
|
376
386
|
await ensureBrowserConnected(job);
|
|
377
|
-
return spawnCommand(
|
|
387
|
+
return spawnCommand(AGENT_BROWSER_BIN, [...browserBaseArgs(job), ...args], options);
|
|
378
388
|
}
|
|
379
389
|
|
|
380
390
|
function parseEvalResult(stdout) {
|
|
@@ -536,27 +546,6 @@ function buildLoginProbeScript(timeoutMs) {
|
|
|
536
546
|
`);
|
|
537
547
|
}
|
|
538
548
|
|
|
539
|
-
function parseSnapshotEntries(snapshot) {
|
|
540
|
-
return snapshot
|
|
541
|
-
.split("\n")
|
|
542
|
-
.map((line) => {
|
|
543
|
-
const refMatch = line.match(/\bref=(e\d+)\b/);
|
|
544
|
-
if (!refMatch) return undefined;
|
|
545
|
-
const kindMatch = line.match(/^\s*-\s*([^\s]+)/);
|
|
546
|
-
const quotedMatch = line.match(/"([^"]*)"/);
|
|
547
|
-
const valueMatch = line.match(/:\s*(.+)$/);
|
|
548
|
-
return {
|
|
549
|
-
line,
|
|
550
|
-
ref: `@${refMatch[1]}`,
|
|
551
|
-
kind: kindMatch ? kindMatch[1] : undefined,
|
|
552
|
-
label: quotedMatch ? quotedMatch[1] : undefined,
|
|
553
|
-
value: valueMatch ? valueMatch[1].trim() : undefined,
|
|
554
|
-
disabled: /\bdisabled\b/.test(line),
|
|
555
|
-
};
|
|
556
|
-
})
|
|
557
|
-
.filter(Boolean);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
549
|
function findEntry(snapshot, predicate) {
|
|
561
550
|
return parseSnapshotEntries(snapshot).find(predicate);
|
|
562
551
|
}
|
|
@@ -569,14 +558,21 @@ function findLastEntry(snapshot, predicate) {
|
|
|
569
558
|
return undefined;
|
|
570
559
|
}
|
|
571
560
|
|
|
572
|
-
function matchesModelFamilyButton(candidate, family) {
|
|
573
|
-
return candidate.kind === "button" && typeof candidate.label === "string" && candidate.label.startsWith(MODEL_FAMILY_PREFIX[family]) && !candidate.disabled;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
561
|
function titleCase(value) {
|
|
577
562
|
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
578
563
|
}
|
|
579
564
|
|
|
565
|
+
function matchesModelFamilyLabel(label, family) {
|
|
566
|
+
const normalized = String(label || "");
|
|
567
|
+
const prefix = MODEL_FAMILY_PREFIX[family];
|
|
568
|
+
const exact = prefix.trim();
|
|
569
|
+
return normalized === exact || normalized.startsWith(prefix) || normalized.startsWith(`${exact},`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function matchesModelFamilyButton(candidate, family) {
|
|
573
|
+
return candidate.kind === "button" && typeof candidate.label === "string" && matchesModelFamilyLabel(candidate.label, family) && !candidate.disabled;
|
|
574
|
+
}
|
|
575
|
+
|
|
580
576
|
function requestedEffortLabel(job) {
|
|
581
577
|
return job.effort ? titleCase(job.effort) : undefined;
|
|
582
578
|
}
|
|
@@ -609,9 +605,9 @@ function snapshotHasModelConfigurationUi(snapshot) {
|
|
|
609
605
|
entries
|
|
610
606
|
.filter((entry) => entry.kind === "button" && typeof entry.label === "string")
|
|
611
607
|
.flatMap((entry) =>
|
|
612
|
-
Object.
|
|
613
|
-
.filter((
|
|
614
|
-
.map((
|
|
608
|
+
Object.keys(MODEL_FAMILY_PREFIX)
|
|
609
|
+
.filter((family) => matchesModelFamilyLabel(entry.label, family))
|
|
610
|
+
.map((family) => family),
|
|
615
611
|
),
|
|
616
612
|
);
|
|
617
613
|
const hasCloseButton = entries.some((entry) => entry.kind === "button" && entry.label === CHATGPT_LABELS.close && !entry.disabled);
|
|
@@ -624,18 +620,35 @@ function snapshotHasModelConfigurationUi(snapshot) {
|
|
|
624
620
|
function snapshotStronglyMatchesRequestedModel(snapshot, job) {
|
|
625
621
|
const entries = parseSnapshotEntries(snapshot);
|
|
626
622
|
const familyMatched = entries.some((entry) => matchesModelFamilyButton(entry, job.chatModelFamily));
|
|
623
|
+
const effortLabel = requestedEffortLabel(job);
|
|
627
624
|
if (job.chatModelFamily === "thinking") {
|
|
628
|
-
return familyMatched || effortSelectionVisible(snapshot,
|
|
625
|
+
return familyMatched || effortSelectionVisible(snapshot, effortLabel);
|
|
629
626
|
}
|
|
630
627
|
if (job.chatModelFamily === "pro") {
|
|
631
|
-
return familyMatched;
|
|
628
|
+
return effortLabel ? familyMatched && effortSelectionVisible(snapshot, effortLabel) : familyMatched;
|
|
632
629
|
}
|
|
633
630
|
return familyMatched;
|
|
634
631
|
}
|
|
635
632
|
|
|
633
|
+
function thinkingSelectionVisible(snapshot) {
|
|
634
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
635
|
+
return entries.some((entry) => !entry.disabled && entry.kind === "button" && matchesModelFamilyLabel(entry.label, "thinking"));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function composerControlsVisible(snapshot) {
|
|
639
|
+
const entries = parseSnapshotEntries(snapshot);
|
|
640
|
+
const hasComposer = entries.some(
|
|
641
|
+
(entry) => entry.kind === "textbox" && entry.label === CHATGPT_LABELS.composer && !entry.disabled,
|
|
642
|
+
);
|
|
643
|
+
const hasAddFiles = entries.some(
|
|
644
|
+
(entry) => entry.kind === "button" && entry.label === CHATGPT_LABELS.addFiles && !entry.disabled,
|
|
645
|
+
);
|
|
646
|
+
return hasComposer && hasAddFiles;
|
|
647
|
+
}
|
|
648
|
+
|
|
636
649
|
function snapshotWeaklyMatchesRequestedModel(snapshot, job) {
|
|
637
650
|
if (job.chatModelFamily === "thinking") {
|
|
638
|
-
return effortSelectionVisible(snapshot, requestedEffortLabel(job));
|
|
651
|
+
return effortSelectionVisible(snapshot, requestedEffortLabel(job)) || thinkingSelectionVisible(snapshot);
|
|
639
652
|
}
|
|
640
653
|
if (job.chatModelFamily === "pro") {
|
|
641
654
|
return !thinkingChipVisible(snapshot);
|
|
@@ -829,6 +842,17 @@ function detectUploadErrorText(text) {
|
|
|
829
842
|
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
830
843
|
}
|
|
831
844
|
|
|
845
|
+
function detectResponseFailureText(text) {
|
|
846
|
+
const patterns = [
|
|
847
|
+
"Message delivery timed out",
|
|
848
|
+
"A network error occurred",
|
|
849
|
+
"An error occurred while connecting to the websocket",
|
|
850
|
+
"There was an error generating a response",
|
|
851
|
+
"Something went wrong while generating the response",
|
|
852
|
+
];
|
|
853
|
+
return patterns.find((pattern) => text.toLowerCase().includes(pattern.toLowerCase()));
|
|
854
|
+
}
|
|
855
|
+
|
|
832
856
|
function composerSnapshotSlice(snapshot) {
|
|
833
857
|
const lines = snapshot.split("\n");
|
|
834
858
|
let composerIndex = -1;
|
|
@@ -941,6 +965,54 @@ async function openModelConfiguration(job) {
|
|
|
941
965
|
throw new Error("Could not open model configuration UI");
|
|
942
966
|
}
|
|
943
967
|
|
|
968
|
+
async function waitForModelConfigurationToSettle(job, options = {}) {
|
|
969
|
+
const deadline = Date.now() + MODEL_CONFIGURATION_SETTLE_TIMEOUT_MS;
|
|
970
|
+
let lastCloseAttemptAt = 0;
|
|
971
|
+
let fallbackLogged = false;
|
|
972
|
+
let lastSnapshot = "";
|
|
973
|
+
|
|
974
|
+
while (Date.now() < deadline) {
|
|
975
|
+
const snapshot = await snapshotText(job);
|
|
976
|
+
lastSnapshot = snapshot;
|
|
977
|
+
const configurationUiVisible = snapshotHasModelConfigurationUi(snapshot);
|
|
978
|
+
|
|
979
|
+
if (!configurationUiVisible) {
|
|
980
|
+
if (snapshotWeaklyMatchesRequestedModel(snapshot, job)) return;
|
|
981
|
+
if (options.stronglyVerified) {
|
|
982
|
+
if (!fallbackLogged) {
|
|
983
|
+
fallbackLogged = true;
|
|
984
|
+
await log(`Model configuration closed after strong in-dialog verification for family=${job.chatModelFamily} effort=${job.effort || "(none)"}`);
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (!configurationUiVisible && composerControlsVisible(snapshot) && options.stronglyVerified) {
|
|
991
|
+
if (!fallbackLogged) {
|
|
992
|
+
fallbackLogged = true;
|
|
993
|
+
await log(`Composer became usable after strong in-dialog verification for family=${job.chatModelFamily} effort=${job.effort || "(none)"}`);
|
|
994
|
+
}
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (Date.now() - lastCloseAttemptAt >= MODEL_CONFIGURATION_CLOSE_RETRY_MS) {
|
|
999
|
+
lastCloseAttemptAt = Date.now();
|
|
1000
|
+
if (!(await maybeClickLabeledEntry(job, CHATGPT_LABELS.close, { kind: "button" }))) {
|
|
1001
|
+
await agentBrowser(job, "press", "Escape").catch(() => undefined);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
await sleep(MODEL_CONFIGURATION_SETTLE_POLL_MS);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (options.stronglyVerified && lastSnapshot && !snapshotHasModelConfigurationUi(lastSnapshot)) {
|
|
1009
|
+
await log(`Model configuration closed only after settle-timeout for family=${job.chatModelFamily} effort=${job.effort || "(none)"}`);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
throw new Error(`Could not verify requested model settings after configuration for ${job.chatModelFamily}`);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
944
1016
|
async function configureModel(job) {
|
|
945
1017
|
const initialSnapshot = await snapshotText(job);
|
|
946
1018
|
if (snapshotStronglyMatchesRequestedModel(initialSnapshot, job)) {
|
|
@@ -950,6 +1022,7 @@ async function configureModel(job) {
|
|
|
950
1022
|
|
|
951
1023
|
await log(`Configuring model family=${job.chatModelFamily} effort=${job.effort || "(none)"}`);
|
|
952
1024
|
let familySnapshot = await openModelConfiguration(job);
|
|
1025
|
+
let verificationSnapshot = familySnapshot;
|
|
953
1026
|
|
|
954
1027
|
let familyEntry = findEntry(familySnapshot, (candidate) => matchesModelFamilyButton(candidate, job.chatModelFamily));
|
|
955
1028
|
if (!familyEntry && snapshotStronglyMatchesRequestedModel(familySnapshot, job)) {
|
|
@@ -963,6 +1036,7 @@ async function configureModel(job) {
|
|
|
963
1036
|
await clickRef(job, familyEntry.ref);
|
|
964
1037
|
await agentBrowser(job, "wait", "800");
|
|
965
1038
|
familySnapshot = await snapshotText(job);
|
|
1039
|
+
verificationSnapshot = familySnapshot;
|
|
966
1040
|
}
|
|
967
1041
|
|
|
968
1042
|
if (job.chatModelFamily === "thinking" || job.chatModelFamily === "pro") {
|
|
@@ -976,6 +1050,7 @@ async function configureModel(job) {
|
|
|
976
1050
|
await clickLabeledEntry(job, effortLabel, { kind: "option" });
|
|
977
1051
|
await agentBrowser(job, "wait", "400");
|
|
978
1052
|
const effortSnapshot = await snapshotText(job);
|
|
1053
|
+
verificationSnapshot = effortSnapshot;
|
|
979
1054
|
const selectedEffort = findEntry(
|
|
980
1055
|
effortSnapshot,
|
|
981
1056
|
(candidate) => candidate.kind === "combobox" && candidate.value === effortLabel && !candidate.disabled,
|
|
@@ -988,17 +1063,18 @@ async function configureModel(job) {
|
|
|
988
1063
|
|
|
989
1064
|
if (job.chatModelFamily === "instant" && job.autoSwitchToThinking) {
|
|
990
1065
|
await maybeClickLabeledEntry(job, CHATGPT_LABELS.autoSwitchToThinking);
|
|
1066
|
+
verificationSnapshot = await snapshotText(job);
|
|
991
1067
|
}
|
|
992
1068
|
|
|
993
|
-
|
|
994
|
-
|
|
1069
|
+
const stronglyVerified = snapshotStronglyMatchesRequestedModel(verificationSnapshot, job);
|
|
1070
|
+
if (!stronglyVerified) {
|
|
1071
|
+
throw new Error(`Could not verify requested model settings in configuration UI for ${job.chatModelFamily}`);
|
|
995
1072
|
}
|
|
996
|
-
await agentBrowser(job, "wait", "500");
|
|
997
1073
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
throw new Error(`Could not verify requested model settings after configuration for ${job.chatModelFamily}`);
|
|
1074
|
+
if (!(await maybeClickLabeledEntry(job, CHATGPT_LABELS.close, { kind: "button" }))) {
|
|
1075
|
+
await agentBrowser(job, "press", "Escape").catch(() => undefined);
|
|
1001
1076
|
}
|
|
1077
|
+
await waitForModelConfigurationToSettle(job, { stronglyVerified });
|
|
1002
1078
|
}
|
|
1003
1079
|
|
|
1004
1080
|
async function uploadArchive(job) {
|
|
@@ -1120,17 +1196,39 @@ async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
|
1120
1196
|
const timeoutAt = Date.now() + job.config.worker.completionTimeoutMs;
|
|
1121
1197
|
let lastText = "";
|
|
1122
1198
|
let stableCount = 0;
|
|
1199
|
+
let retriedAfterFailure = false;
|
|
1123
1200
|
|
|
1124
1201
|
while (Date.now() < timeoutAt) {
|
|
1125
1202
|
await heartbeat();
|
|
1126
|
-
const snapshot = await snapshotText(job);
|
|
1203
|
+
const [snapshot, body] = await Promise.all([snapshotText(job), pageText(job).catch(() => "")]);
|
|
1127
1204
|
const hasStopStreaming = snapshot.includes("Stop streaming");
|
|
1205
|
+
const hasRetryButton = snapshot.includes('button "Retry"');
|
|
1128
1206
|
const copyResponseCount = (snapshot.match(/Copy response/g) || []).length;
|
|
1207
|
+
const responseFailureText = detectResponseFailureText(`${snapshot}\n${body}`);
|
|
1129
1208
|
const messages = await assistantMessages(job);
|
|
1130
1209
|
const targetMessage = messages[baselineAssistantCount];
|
|
1131
1210
|
const targetText = targetMessage?.text || "";
|
|
1132
1211
|
const hasTargetCopyResponse = copyResponseCount > baselineAssistantCount;
|
|
1133
1212
|
|
|
1213
|
+
if (!hasStopStreaming && hasRetryButton && responseFailureText) {
|
|
1214
|
+
if (!retriedAfterFailure) {
|
|
1215
|
+
const retryEntry = findEntry(
|
|
1216
|
+
snapshot,
|
|
1217
|
+
(candidate) => candidate.kind === "button" && candidate.label === "Retry" && !candidate.disabled,
|
|
1218
|
+
);
|
|
1219
|
+
if (retryEntry) {
|
|
1220
|
+
retriedAfterFailure = true;
|
|
1221
|
+
lastText = "";
|
|
1222
|
+
stableCount = 0;
|
|
1223
|
+
await log(`Response delivery failed (${responseFailureText}); clicking Retry once`);
|
|
1224
|
+
await clickRef(job, retryEntry.ref);
|
|
1225
|
+
await agentBrowser(job, "wait", "1000").catch(() => undefined);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
throw new Error(`ChatGPT response failed: ${responseFailureText}`);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1134
1232
|
if (!hasStopStreaming && hasTargetCopyResponse && targetText) {
|
|
1135
1233
|
if (targetText === lastText) stableCount += 1;
|
|
1136
1234
|
else stableCount = 1;
|
|
@@ -1156,14 +1254,6 @@ async function detectType(path) {
|
|
|
1156
1254
|
return result.stdout || "unknown";
|
|
1157
1255
|
}
|
|
1158
1256
|
|
|
1159
|
-
function isLikelyArtifactLabel(label) {
|
|
1160
|
-
const normalized = String(label || "").trim();
|
|
1161
|
-
if (!normalized) return false;
|
|
1162
|
-
const upper = normalized.toUpperCase();
|
|
1163
|
-
if (upper === "ATTACHED" || upper === "DONE") return true;
|
|
1164
|
-
return /(?:^|[^\w])[^\n]*\.[A-Za-z0-9]{1,12}(?:$|[^\w])/.test(normalized);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
1257
|
function preferredArtifactName(label, index) {
|
|
1168
1258
|
const normalized = String(label || "").trim();
|
|
1169
1259
|
const fileNameMatch = normalized.match(/([A-Za-z0-9._-]+\.[A-Za-z0-9]{1,12})(?!.*[A-Za-z0-9._-]+\.[A-Za-z0-9]{1,12})/);
|
|
@@ -1171,57 +1261,82 @@ function preferredArtifactName(label, index) {
|
|
|
1171
1261
|
return `artifact-${String(index + 1).padStart(2, "0")}`;
|
|
1172
1262
|
}
|
|
1173
1263
|
|
|
1174
|
-
function
|
|
1175
|
-
const excluded = new Set([
|
|
1176
|
-
"Copy response",
|
|
1177
|
-
"Good response",
|
|
1178
|
-
"Bad response",
|
|
1179
|
-
"Share",
|
|
1180
|
-
"Switch model",
|
|
1181
|
-
"More actions",
|
|
1182
|
-
CHATGPT_LABELS.addFiles,
|
|
1183
|
-
"Start dictation",
|
|
1184
|
-
"Start Voice",
|
|
1185
|
-
"Model selector",
|
|
1186
|
-
"Open conversation options",
|
|
1187
|
-
"Scroll to bottom",
|
|
1188
|
-
CHATGPT_LABELS.close,
|
|
1189
|
-
]);
|
|
1190
|
-
|
|
1191
|
-
const seen = new Set();
|
|
1192
|
-
const candidates = [];
|
|
1193
|
-
for (const entry of entries) {
|
|
1194
|
-
if (!entry.label) continue;
|
|
1195
|
-
if (excluded.has(entry.label)) continue;
|
|
1196
|
-
if (entry.label.startsWith("Thought for ")) continue;
|
|
1197
|
-
if (entry.kind !== "button" && entry.kind !== "link") continue;
|
|
1198
|
-
if (!isLikelyArtifactLabel(entry.label)) continue;
|
|
1199
|
-
if (seen.has(entry.label)) continue;
|
|
1200
|
-
seen.add(entry.label);
|
|
1201
|
-
candidates.push({ label: entry.label });
|
|
1202
|
-
}
|
|
1203
|
-
return candidates;
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
async function collectArtifactCandidates(job, responseIndex) {
|
|
1264
|
+
async function collectArtifactCandidates(job, responseIndex, responseText = "") {
|
|
1207
1265
|
const snapshot = await snapshotText(job);
|
|
1208
1266
|
const targetSlice = assistantSnapshotSlice(snapshot, responseIndex);
|
|
1209
1267
|
if (!targetSlice) return { snapshot, targetSlice, candidates: [] };
|
|
1268
|
+
|
|
1269
|
+
const structural = await evalPage(
|
|
1270
|
+
job,
|
|
1271
|
+
toJsonScript(`
|
|
1272
|
+
const normalize = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
|
1273
|
+
const genericArtifactLabels = new Set(${JSON.stringify(GENERIC_ARTIFACT_LABELS)});
|
|
1274
|
+
const fileLabelPattern = new RegExp(${JSON.stringify(FILE_LABEL_PATTERN_SOURCE)});
|
|
1275
|
+
const isFileLabel = (value) => {
|
|
1276
|
+
const normalized = normalize(value);
|
|
1277
|
+
if (!normalized) return false;
|
|
1278
|
+
if (genericArtifactLabels.has(normalized.toUpperCase())) return true;
|
|
1279
|
+
return fileLabelPattern.test(normalized);
|
|
1280
|
+
};
|
|
1281
|
+
const headings = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,[role="heading"]'))
|
|
1282
|
+
.filter((el) => normalize(el.textContent) === 'ChatGPT said:');
|
|
1283
|
+
const host = headings[${responseIndex}]?.nextElementSibling;
|
|
1284
|
+
if (!host) return { candidates: [] };
|
|
1285
|
+
|
|
1286
|
+
const fileButtons = (node) => node
|
|
1287
|
+
? Array.from(node.querySelectorAll('button, a')).map((candidate) => normalize(candidate.textContent)).filter(isFileLabel)
|
|
1288
|
+
: [];
|
|
1289
|
+
const otherTextLength = (text, labels) => {
|
|
1290
|
+
let remaining = normalize(text);
|
|
1291
|
+
for (const label of labels || []) {
|
|
1292
|
+
remaining = normalize(remaining.replaceAll(label, ' '));
|
|
1293
|
+
}
|
|
1294
|
+
remaining = normalize(remaining.replaceAll('Coding Citation', ' '));
|
|
1295
|
+
return remaining.length;
|
|
1296
|
+
};
|
|
1297
|
+
const focusableFor = (node) => node?.closest('[tabindex]');
|
|
1298
|
+
|
|
1299
|
+
const candidates = Array.from(host.querySelectorAll('button, a'))
|
|
1300
|
+
.map((button) => {
|
|
1301
|
+
const label = normalize(button.textContent);
|
|
1302
|
+
if (!isFileLabel(label)) return null;
|
|
1303
|
+
const paragraph = button.closest('p');
|
|
1304
|
+
const listItem = button.closest('li');
|
|
1305
|
+
const focusable = focusableFor(button);
|
|
1306
|
+
const paragraphFileLabels = fileButtons(paragraph);
|
|
1307
|
+
const focusableFileLabels = fileButtons(focusable);
|
|
1308
|
+
return {
|
|
1309
|
+
label,
|
|
1310
|
+
paragraphText: normalize(paragraph?.textContent),
|
|
1311
|
+
listItemText: normalize(listItem?.textContent),
|
|
1312
|
+
paragraphFileButtonCount: paragraphFileLabels.length,
|
|
1313
|
+
paragraphOtherTextLength: otherTextLength(paragraph?.textContent, paragraphFileLabels),
|
|
1314
|
+
listItemFileButtonCount: fileButtons(listItem).length,
|
|
1315
|
+
focusableFileButtonCount: focusableFileLabels.length,
|
|
1316
|
+
focusableOtherTextLength: otherTextLength(focusable?.textContent, focusableFileLabels),
|
|
1317
|
+
};
|
|
1318
|
+
})
|
|
1319
|
+
.filter(Boolean);
|
|
1320
|
+
|
|
1321
|
+
return { candidates };
|
|
1322
|
+
`),
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1210
1325
|
return {
|
|
1211
1326
|
snapshot,
|
|
1212
1327
|
targetSlice,
|
|
1213
|
-
candidates:
|
|
1328
|
+
candidates: filterStructuralArtifactCandidates(structural?.candidates || []),
|
|
1214
1329
|
};
|
|
1215
1330
|
}
|
|
1216
1331
|
|
|
1217
|
-
async function waitForStableArtifactCandidates(job, responseIndex) {
|
|
1332
|
+
async function waitForStableArtifactCandidates(job, responseIndex, responseText = "") {
|
|
1218
1333
|
const deadline = Date.now() + ARTIFACT_CANDIDATE_STABILITY_TIMEOUT_MS;
|
|
1219
1334
|
let lastSignature;
|
|
1220
1335
|
let stablePolls = 0;
|
|
1221
1336
|
let latest = { snapshot: "", targetSlice: undefined, candidates: [] };
|
|
1222
1337
|
|
|
1223
1338
|
while (Date.now() < deadline) {
|
|
1224
|
-
latest = await collectArtifactCandidates(job, responseIndex);
|
|
1339
|
+
latest = await collectArtifactCandidates(job, responseIndex, responseText);
|
|
1225
1340
|
const signature = latest.candidates.map((candidate) => candidate.label).join("\n");
|
|
1226
1341
|
if (signature === lastSignature) stablePolls += 1;
|
|
1227
1342
|
else {
|
|
@@ -1236,12 +1351,12 @@ async function waitForStableArtifactCandidates(job, responseIndex) {
|
|
|
1236
1351
|
return latest;
|
|
1237
1352
|
}
|
|
1238
1353
|
|
|
1239
|
-
async function reopenConversationForArtifacts(job, responseIndex, reason) {
|
|
1354
|
+
async function reopenConversationForArtifacts(job, responseIndex, responseText, reason) {
|
|
1240
1355
|
const targetUrl = job.chatUrl || stripQuery(await currentUrl(job));
|
|
1241
1356
|
await log(`Reopening conversation before artifact capture (${reason}): ${targetUrl}`);
|
|
1242
1357
|
await agentBrowser(job, "open", targetUrl);
|
|
1243
1358
|
await agentBrowser(job, "wait", "1500");
|
|
1244
|
-
return waitForStableArtifactCandidates(job, responseIndex);
|
|
1359
|
+
return waitForStableArtifactCandidates(job, responseIndex, responseText);
|
|
1245
1360
|
}
|
|
1246
1361
|
|
|
1247
1362
|
async function withHeartbeatWhile(task, intervalMs = ARTIFACT_DOWNLOAD_HEARTBEAT_MS) {
|
|
@@ -1273,14 +1388,14 @@ async function flushArtifactsState(artifacts) {
|
|
|
1273
1388
|
}));
|
|
1274
1389
|
}
|
|
1275
1390
|
|
|
1276
|
-
async function downloadArtifacts(job, responseIndex) {
|
|
1391
|
+
async function downloadArtifacts(job, responseIndex, responseText = "") {
|
|
1277
1392
|
if (!job.config.artifacts.capture) {
|
|
1278
1393
|
await secureWriteText(`${jobDir}/artifacts.json`, "[]\n");
|
|
1279
1394
|
await mutateJob((current) => ({ ...current, artifactPaths: [] }));
|
|
1280
1395
|
return [];
|
|
1281
1396
|
}
|
|
1282
1397
|
|
|
1283
|
-
const { targetSlice, candidates } = await reopenConversationForArtifacts(job, responseIndex, "initial");
|
|
1398
|
+
const { targetSlice, candidates } = await reopenConversationForArtifacts(job, responseIndex, responseText, "initial");
|
|
1284
1399
|
if (!targetSlice) {
|
|
1285
1400
|
await log(`No assistant response found in snapshot for response index ${responseIndex}`);
|
|
1286
1401
|
await secureWriteText(`${jobDir}/artifacts.json`, "[]\n");
|
|
@@ -1342,7 +1457,7 @@ async function downloadArtifacts(job, responseIndex) {
|
|
|
1342
1457
|
if (attempt >= ARTIFACT_DOWNLOAD_MAX_ATTEMPTS) {
|
|
1343
1458
|
artifacts.push({ displayName: candidate.label, unconfirmed: true, error: message });
|
|
1344
1459
|
} else {
|
|
1345
|
-
await reopenConversationForArtifacts(job, responseIndex, `retry ${attempt + 1} for ${candidate.label}`);
|
|
1460
|
+
await reopenConversationForArtifacts(job, responseIndex, responseText, `retry ${attempt + 1} for ${candidate.label}`);
|
|
1346
1461
|
await sleep(1_000);
|
|
1347
1462
|
}
|
|
1348
1463
|
} finally {
|
|
@@ -1407,7 +1522,7 @@ async function run() {
|
|
|
1407
1522
|
currentJob = await mutateJob((job) => ({ ...job, ...phasePatch("extracting_response", { heartbeatAt: new Date().toISOString() }) }));
|
|
1408
1523
|
await secureWriteText(currentJob.responsePath, `${completion.responseText.trim()}\n`);
|
|
1409
1524
|
currentJob = await mutateJob((job) => ({ ...job, ...phasePatch("downloading_artifacts", { heartbeatAt: new Date().toISOString() }) }));
|
|
1410
|
-
const artifacts = await downloadArtifacts(currentJob, completion.responseIndex);
|
|
1525
|
+
const artifacts = await downloadArtifacts(currentJob, completion.responseIndex, completion.responseText);
|
|
1411
1526
|
const artifactFailureCount = artifacts.filter((artifact) => artifact.unconfirmed || artifact.error).length;
|
|
1412
1527
|
|
|
1413
1528
|
await heartbeat(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
]
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
|
-
"check:oracle-extension": "node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@sinclair/typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
44
|
-
"sanity:oracle": "
|
|
43
|
+
"check:oracle-extension": "node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@sinclair/typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
44
|
+
"sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
|
|
45
45
|
"pack:check": "npm pack --dry-run"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|