pi-oracle 0.5.0 → 0.6.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/CHANGELOG.md +22 -0
- package/README.md +21 -7
- package/docs/ORACLE_DESIGN.md +25 -15
- package/extensions/oracle/index.ts +1 -1
- package/extensions/oracle/lib/auth.ts +50 -0
- package/extensions/oracle/lib/commands.ts +51 -50
- package/extensions/oracle/lib/config.ts +3 -1
- package/extensions/oracle/lib/jobs.ts +14 -3
- package/extensions/oracle/lib/poller.ts +8 -2
- package/extensions/oracle/lib/runtime.ts +129 -2
- package/extensions/oracle/lib/tools.ts +145 -25
- package/extensions/oracle/shared/job-observability-helpers.mjs +2 -2
- package/extensions/oracle/worker/auth-flow-helpers.mjs +1 -1
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +106 -41
- package/extensions/oracle/worker/run-job.mjs +17 -13
- package/package.json +6 -2
- package/prompts/oracle-followup.md +48 -0
- package/prompts/oracle.md +7 -6
|
@@ -7,7 +7,7 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { spawn } from "node:child_process";
|
|
8
8
|
import { constants as fsConstants, existsSync, realpathSync, readFileSync } from "node:fs";
|
|
9
9
|
import { access, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
10
|
-
import { dirname, join } from "node:path";
|
|
10
|
+
import { delimiter, dirname, join } from "node:path";
|
|
11
11
|
import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
|
|
12
12
|
import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
|
|
13
13
|
import type { OracleConfig } from "./config.js";
|
|
@@ -21,6 +21,14 @@ const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/ag
|
|
|
21
21
|
) || "agent-browser";
|
|
22
22
|
const PROFILE_CLONE_TIMEOUT_MS = 120_000;
|
|
23
23
|
const ORACLE_SUBPROCESS_KILL_GRACE_MS = 2_000;
|
|
24
|
+
const WORKSPACE_ROOT_MARKERS = [
|
|
25
|
+
".pi/extensions/oracle.json",
|
|
26
|
+
] as const;
|
|
27
|
+
const REQUIRED_ORACLE_DEPENDENCIES = [
|
|
28
|
+
{ name: "agent-browser", command: AGENT_BROWSER_BIN },
|
|
29
|
+
{ name: "tar", command: "tar" },
|
|
30
|
+
{ name: "zstd", command: "zstd" },
|
|
31
|
+
] as const;
|
|
24
32
|
|
|
25
33
|
export interface OracleRuntimeLeaseMetadata {
|
|
26
34
|
jobId: string;
|
|
@@ -51,7 +59,7 @@ export interface OracleConversationLeaseAttempt {
|
|
|
51
59
|
blocker?: OracleConversationLeaseMetadata;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
function resolveRealCwd(cwd: string): string {
|
|
55
63
|
try {
|
|
56
64
|
return realpathSync(cwd);
|
|
57
65
|
} catch {
|
|
@@ -59,6 +67,26 @@ export function getProjectId(cwd: string): string {
|
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
function hasWorkspaceRootMarker(path: string): boolean {
|
|
71
|
+
return WORKSPACE_ROOT_MARKERS.some((marker) => existsSync(join(path, marker)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveWorkspaceRoot(realCwd: string): string {
|
|
75
|
+
let current = realCwd;
|
|
76
|
+
let nearestMarkerRoot: string | undefined;
|
|
77
|
+
while (true) {
|
|
78
|
+
if (existsSync(join(current, ".git"))) return current;
|
|
79
|
+
if (!nearestMarkerRoot && hasWorkspaceRootMarker(current)) nearestMarkerRoot = current;
|
|
80
|
+
const parent = dirname(current);
|
|
81
|
+
if (parent === current) return nearestMarkerRoot ?? realCwd;
|
|
82
|
+
current = parent;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getProjectId(cwd: string): string {
|
|
87
|
+
return resolveWorkspaceRoot(resolveRealCwd(cwd));
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
export function hasPersistedSessionFile(originSessionFile: string | undefined): originSessionFile is string {
|
|
63
91
|
return Boolean(originSessionFile);
|
|
64
92
|
}
|
|
@@ -110,6 +138,99 @@ function unreadableAuthSeedProfileMessage(seedDir: string): string {
|
|
|
110
138
|
return `Oracle auth seed profile is not readable: ${seedDir}. Fix its permissions or rerun /oracle-auth.`;
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
function missingBrowserExecutableMessage(executablePath: string): string {
|
|
142
|
+
return `Configured oracle browser executable does not exist: ${executablePath}. Fix browser.executablePath or install Chrome there.`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function nonExecutableBrowserMessage(executablePath: string): string {
|
|
146
|
+
return `Configured oracle browser executable is not executable: ${executablePath}. Fix browser.executablePath permissions or point it at a runnable Chrome binary.`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function missingLocalDependencyMessage(name: string): string {
|
|
150
|
+
return `Oracle prerequisite not found on PATH: ${name}. Install ${name} and retry.`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unwritableOracleDirectoryMessage(label: "runtime profiles" | "jobs", path: string): string {
|
|
154
|
+
return `Oracle ${label} directory is not writable: ${path}. Fix its permissions or configure a writable path, then retry.`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function resolveExecutableOnPath(command: string): Promise<string | undefined> {
|
|
158
|
+
if (!command) return undefined;
|
|
159
|
+
if (command.includes("/")) {
|
|
160
|
+
try {
|
|
161
|
+
await access(command, fsConstants.X_OK);
|
|
162
|
+
return command;
|
|
163
|
+
} catch {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const pathValue = process.env.PATH ?? "";
|
|
169
|
+
for (const segment of pathValue.split(delimiter)) {
|
|
170
|
+
if (!segment) continue;
|
|
171
|
+
const candidate = join(segment, command);
|
|
172
|
+
try {
|
|
173
|
+
await access(candidate, fsConstants.X_OK);
|
|
174
|
+
return candidate;
|
|
175
|
+
} catch {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function assertConfiguredBrowserExecutableReady(executablePath: string | undefined): Promise<void> {
|
|
183
|
+
if (!executablePath) return;
|
|
184
|
+
let executableStats;
|
|
185
|
+
try {
|
|
186
|
+
executableStats = await stat(executablePath);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
189
|
+
if (code === "ENOENT") throw new Error(missingBrowserExecutableMessage(executablePath));
|
|
190
|
+
if (code === "EACCES" || code === "EPERM") throw new Error(nonExecutableBrowserMessage(executablePath));
|
|
191
|
+
throw new Error(`Failed to inspect configured oracle browser executable ${executablePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (!executableStats.isFile()) {
|
|
195
|
+
throw new Error(nonExecutableBrowserMessage(executablePath));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await access(executablePath, fsConstants.X_OK);
|
|
200
|
+
} catch {
|
|
201
|
+
throw new Error(nonExecutableBrowserMessage(executablePath));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function assertRequiredLocalDependencyReady(name: string, command: string): Promise<void> {
|
|
206
|
+
const resolved = await resolveExecutableOnPath(command);
|
|
207
|
+
if (!resolved) throw new Error(missingLocalDependencyMessage(name));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function assertWritableDirectory(path: string, label: "runtime profiles" | "jobs"): Promise<void> {
|
|
211
|
+
try {
|
|
212
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
213
|
+
} catch {
|
|
214
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let directoryStats;
|
|
218
|
+
try {
|
|
219
|
+
directoryStats = await stat(path);
|
|
220
|
+
} catch {
|
|
221
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
222
|
+
}
|
|
223
|
+
if (!directoryStats.isDirectory()) {
|
|
224
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await access(path, fsConstants.W_OK | fsConstants.X_OK);
|
|
229
|
+
} catch {
|
|
230
|
+
throw new Error(unwritableOracleDirectoryMessage(label, path));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
113
234
|
export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Promise<void> {
|
|
114
235
|
const seedDir = config.browser.authSeedProfileDir;
|
|
115
236
|
let seedStats;
|
|
@@ -135,6 +256,12 @@ export async function assertOracleAuthSeedProfileReady(config: OracleConfig): Pr
|
|
|
135
256
|
|
|
136
257
|
export async function assertOracleSubmitPrerequisites(config: OracleConfig): Promise<void> {
|
|
137
258
|
await assertOracleAuthSeedProfileReady(config);
|
|
259
|
+
await assertConfiguredBrowserExecutableReady(config.browser.executablePath);
|
|
260
|
+
for (const dependency of REQUIRED_ORACLE_DEPENDENCIES) {
|
|
261
|
+
await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
|
|
262
|
+
}
|
|
263
|
+
await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
|
|
264
|
+
await assertWritableDirectory(ORACLE_JOBS_DIR, "jobs");
|
|
138
265
|
}
|
|
139
266
|
|
|
140
267
|
export function getSeedGeneration(config: OracleConfig): string | undefined {
|
|
@@ -7,6 +7,7 @@ import { randomUUID } from "node:crypto";
|
|
|
7
7
|
import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { basename, join, posix } from "node:path";
|
|
10
|
+
import { runOracleAuthBootstrap } from "./auth.js";
|
|
10
11
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
11
12
|
import { Type } from "@sinclair/typebox";
|
|
12
13
|
import { formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
|
|
@@ -58,7 +59,11 @@ import {
|
|
|
58
59
|
|
|
59
60
|
const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
60
61
|
prompt: Type.String({ description: "Prompt text to send to ChatGPT web." }),
|
|
61
|
-
files: Type.Array(Type.String({
|
|
62
|
+
files: Type.Array(Type.String({
|
|
63
|
+
description: "Project-relative file or directory path to include in the archive.",
|
|
64
|
+
minLength: 1,
|
|
65
|
+
pattern: ".*\\S.*",
|
|
66
|
+
}), {
|
|
62
67
|
description: "Exact project-relative files/directories to include in the oracle archive.",
|
|
63
68
|
minItems: 1,
|
|
64
69
|
}),
|
|
@@ -150,6 +155,20 @@ type ArchiveCreationResult = {
|
|
|
150
155
|
includedEntries: string[];
|
|
151
156
|
};
|
|
152
157
|
|
|
158
|
+
function appendArchiveEntries(target: string[], source: Iterable<string>): void {
|
|
159
|
+
for (const entry of source) target.push(entry);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function mergeArchiveEntryGroups(groups: Iterable<Iterable<string>>): string[] {
|
|
163
|
+
const merged: string[] = [];
|
|
164
|
+
for (const group of groups) appendArchiveEntries(merged, group);
|
|
165
|
+
return merged;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function mergeArchiveEntryGroupsForTesting(groups: Iterable<Iterable<string>>): string[] {
|
|
169
|
+
return mergeArchiveEntryGroups(groups);
|
|
170
|
+
}
|
|
171
|
+
|
|
153
172
|
function pathContainsSequence(relativePath: string, sequence: readonly string[]): boolean {
|
|
154
173
|
const segments = relativePath.split("/").filter(Boolean);
|
|
155
174
|
if (sequence.length === 0 || segments.length < sequence.length) return false;
|
|
@@ -263,7 +282,7 @@ async function expandArchiveEntries(cwd: string, relativePath: string, options?:
|
|
|
263
282
|
for (const child of children.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
264
283
|
const childRelative = child.name;
|
|
265
284
|
if (await shouldExcludeArchiveChild(join(cwd, childRelative), childRelative, child)) continue;
|
|
266
|
-
if (child.isDirectory()) results
|
|
285
|
+
if (child.isDirectory()) appendArchiveEntries(results, await expandArchiveEntries(cwd, childRelative));
|
|
267
286
|
else results.push(childRelative);
|
|
268
287
|
}
|
|
269
288
|
return results;
|
|
@@ -279,7 +298,7 @@ async function expandArchiveEntries(cwd: string, relativePath: string, options?:
|
|
|
279
298
|
for (const child of children.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
280
299
|
const childRelative = posix.join(normalized, child.name);
|
|
281
300
|
if (await shouldExcludeArchiveChild(join(cwd, childRelative), childRelative, child, { forceInclude: options?.forceIncludeSubtree })) continue;
|
|
282
|
-
if (child.isDirectory()) results
|
|
301
|
+
if (child.isDirectory()) appendArchiveEntries(results, await expandArchiveEntries(cwd, childRelative, { forceIncludeSubtree: options?.forceIncludeSubtree }));
|
|
283
302
|
else results.push(childRelative);
|
|
284
303
|
}
|
|
285
304
|
return results;
|
|
@@ -289,11 +308,12 @@ async function resolveExpandedArchiveEntriesFromInputs(
|
|
|
289
308
|
cwd: string,
|
|
290
309
|
entries: Array<{ absolute: string; relative: string }>,
|
|
291
310
|
): Promise<string[]> {
|
|
292
|
-
|
|
311
|
+
const expandedGroups = await Promise.all(entries.map(async (entry) => {
|
|
293
312
|
const statEntry = await lstat(entry.absolute);
|
|
294
313
|
const forceIncludeSubtree = statEntry.isDirectory() && entry.relative !== "." && shouldExcludeArchivePath(entry.relative, true);
|
|
295
314
|
return expandArchiveEntries(cwd, entry.relative, { forceIncludeSubtree });
|
|
296
|
-
}))
|
|
315
|
+
}));
|
|
316
|
+
return Array.from(new Set(mergeArchiveEntryGroups(expandedGroups))).sort();
|
|
297
317
|
}
|
|
298
318
|
|
|
299
319
|
export async function resolveExpandedArchiveEntries(cwd: string, files: string[]): Promise<string[]> {
|
|
@@ -568,7 +588,7 @@ function resolveFollowUp(previousJobId: string | undefined, cwd: string): {
|
|
|
568
588
|
};
|
|
569
589
|
}
|
|
570
590
|
|
|
571
|
-
type OracleToolName = "oracle_submit" | "oracle_read" | "oracle_cancel";
|
|
591
|
+
type OracleToolName = "oracle_auth" | "oracle_submit" | "oracle_read" | "oracle_cancel";
|
|
572
592
|
type OracleToolErrorSource = OracleToolName | "oracle_preflight";
|
|
573
593
|
type OracleQueueSnapshot = { queued: boolean; position?: number; depth?: number };
|
|
574
594
|
type OracleToolErrorDetails = {
|
|
@@ -587,7 +607,7 @@ type OracleToolJobDetailsOptions = {
|
|
|
587
607
|
responseAvailable?: boolean;
|
|
588
608
|
};
|
|
589
609
|
|
|
590
|
-
const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_submit", "oracle_read", "oracle_cancel"]);
|
|
610
|
+
const ORACLE_TOOL_NAMES = new Set<OracleToolName>(["oracle_auth", "oracle_submit", "oracle_read", "oracle_cancel"]);
|
|
591
611
|
|
|
592
612
|
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
593
613
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
@@ -682,7 +702,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
682
702
|
code: "auth_seed_profile_missing",
|
|
683
703
|
message,
|
|
684
704
|
rejectedValue: message.replace(/^Oracle auth seed profile not found: /, "").replace(/\. Run \/oracle-auth first\.$/, ""),
|
|
685
|
-
suggestedNextStep: "
|
|
705
|
+
suggestedNextStep: "Call oracle_auth or run /oracle-auth once, then retry the oracle tool call.",
|
|
686
706
|
};
|
|
687
707
|
}
|
|
688
708
|
|
|
@@ -691,7 +711,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
691
711
|
code: "auth_seed_profile_unreadable",
|
|
692
712
|
message,
|
|
693
713
|
rejectedValue: message.replace(/^Oracle auth seed profile is not readable: /, "").replace(/\. Fix its permissions or rerun \/oracle-auth\.$/, ""),
|
|
694
|
-
suggestedNextStep: "Fix the auth seed profile permissions or rerun /oracle-auth, then retry.",
|
|
714
|
+
suggestedNextStep: "Fix the auth seed profile permissions or call oracle_auth / rerun /oracle-auth once, then retry.",
|
|
695
715
|
};
|
|
696
716
|
}
|
|
697
717
|
|
|
@@ -700,7 +720,7 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
700
720
|
code: "auth_seed_profile_invalid_type",
|
|
701
721
|
message,
|
|
702
722
|
rejectedValue: message.replace(/^Oracle auth seed profile is not a directory: /, "").replace(/\. Remove the invalid path or rerun \/oracle-auth\.$/, ""),
|
|
703
|
-
suggestedNextStep: "Remove the invalid auth seed path or rerun /oracle-auth to recreate it.",
|
|
723
|
+
suggestedNextStep: "Remove the invalid auth seed path or call oracle_auth / rerun /oracle-auth once to recreate it.",
|
|
704
724
|
};
|
|
705
725
|
}
|
|
706
726
|
|
|
@@ -712,6 +732,52 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
712
732
|
};
|
|
713
733
|
}
|
|
714
734
|
|
|
735
|
+
if (message.startsWith("Configured oracle browser executable does not exist: ")) {
|
|
736
|
+
return {
|
|
737
|
+
code: "browser_executable_missing",
|
|
738
|
+
message,
|
|
739
|
+
rejectedValue: message.replace(/^Configured oracle browser executable does not exist: /, "").replace(/\. Fix browser\.executablePath or install Chrome there\.$/, ""),
|
|
740
|
+
suggestedNextStep: "Fix browser.executablePath or install Chrome at that path, then retry.",
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (message.startsWith("Configured oracle browser executable is not executable: ")) {
|
|
745
|
+
return {
|
|
746
|
+
code: "browser_executable_not_executable",
|
|
747
|
+
message,
|
|
748
|
+
rejectedValue: message.replace(/^Configured oracle browser executable is not executable: /, "").replace(/\. Fix browser\.executablePath permissions or point it at a runnable Chrome binary\.$/, ""),
|
|
749
|
+
suggestedNextStep: "Fix browser.executablePath permissions or point it at a runnable Chrome binary, then retry.",
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (message.startsWith("Oracle prerequisite not found on PATH: ")) {
|
|
754
|
+
const rejectedValue = message.replace(/^Oracle prerequisite not found on PATH: /, "").replace(/\. Install .*$/, "");
|
|
755
|
+
return {
|
|
756
|
+
code: "local_dependency_missing",
|
|
757
|
+
message,
|
|
758
|
+
rejectedValue,
|
|
759
|
+
suggestedNextStep: `Install ${rejectedValue || "the missing dependency"} and retry.`,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
|
|
764
|
+
return {
|
|
765
|
+
code: "runtime_profiles_dir_unwritable",
|
|
766
|
+
message,
|
|
767
|
+
rejectedValue: message.replace(/^Oracle runtime profiles directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
|
|
768
|
+
suggestedNextStep: "Fix browser.runtimeProfilesDir permissions or configure a writable directory, then retry.",
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (message.startsWith("Oracle jobs directory is not writable: ")) {
|
|
773
|
+
return {
|
|
774
|
+
code: "jobs_dir_unwritable",
|
|
775
|
+
message,
|
|
776
|
+
rejectedValue: message.replace(/^Oracle jobs directory is not writable: /, "").replace(/\. Fix its permissions or configure a writable path, then retry\.$/, ""),
|
|
777
|
+
suggestedNextStep: "Fix PI_ORACLE_JOBS_DIR permissions or point it at a writable directory, then retry.",
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
715
781
|
if (toolName === "oracle_submit" && message === "oracle_submit requires at least one file or directory to archive") {
|
|
716
782
|
return {
|
|
717
783
|
code: "archive_input_required",
|
|
@@ -720,6 +786,22 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
|
|
|
720
786
|
};
|
|
721
787
|
}
|
|
722
788
|
|
|
789
|
+
if (toolName === "oracle_submit" && message === "Archive input must be a non-empty project-relative path") {
|
|
790
|
+
return {
|
|
791
|
+
code: "archive_input_blank",
|
|
792
|
+
message,
|
|
793
|
+
suggestedNextStep: "Retry with a non-empty project-relative file or directory path. Use '.' only when you intentionally want a whole-repo archive.",
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (toolName === "oracle_submit" && message === "Archive input must use '.' exactly for a whole-repo archive") {
|
|
798
|
+
return {
|
|
799
|
+
code: "archive_input_whole_repo_sentinel_invalid",
|
|
800
|
+
message,
|
|
801
|
+
suggestedNextStep: "If you want a whole-repo archive, pass '.' exactly. Otherwise pass an exact project-relative path without extra padding.",
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
723
805
|
if (toolName === "oracle_submit" && message.startsWith("Archive input does not exist: ")) {
|
|
724
806
|
return {
|
|
725
807
|
code: "archive_input_missing",
|
|
@@ -815,7 +897,13 @@ function buildOracleToolErrorResult(
|
|
|
815
897
|
) {
|
|
816
898
|
const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
|
|
817
899
|
return {
|
|
818
|
-
content: [{
|
|
900
|
+
content: [{
|
|
901
|
+
type: "text" as const,
|
|
902
|
+
text: [
|
|
903
|
+
errorDetails.message,
|
|
904
|
+
errorDetails.suggestedNextStep ? `Suggested next step: ${errorDetails.suggestedNextStep}` : undefined,
|
|
905
|
+
].filter(Boolean).join("\n"),
|
|
906
|
+
}],
|
|
819
907
|
details: {
|
|
820
908
|
job: options?.job ? redactJobDetails(options.job, options.jobDetails) : undefined,
|
|
821
909
|
error: errorDetails,
|
|
@@ -887,15 +975,16 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
|
|
|
887
975
|
try {
|
|
888
976
|
await assertOracleSubmitPrerequisites(config);
|
|
889
977
|
} catch (error) {
|
|
978
|
+
const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, {});
|
|
890
979
|
return {
|
|
891
980
|
ready: false,
|
|
892
981
|
session: { persisted: true, sessionFile },
|
|
893
982
|
config: { ready: true },
|
|
894
983
|
auth: {
|
|
895
|
-
ready:
|
|
984
|
+
ready: !["auth_seed_profile_missing", "auth_seed_profile_unreadable", "auth_seed_profile_invalid_type"].includes(errorDetails.code),
|
|
896
985
|
seedProfileDir: config.browser.authSeedProfileDir,
|
|
897
986
|
},
|
|
898
|
-
error:
|
|
987
|
+
error: errorDetails,
|
|
899
988
|
};
|
|
900
989
|
}
|
|
901
990
|
|
|
@@ -910,7 +999,7 @@ async function runOraclePreflight(ctx: ExtensionContext): Promise<OraclePrefligh
|
|
|
910
999
|
};
|
|
911
1000
|
}
|
|
912
1001
|
|
|
913
|
-
export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void {
|
|
1002
|
+
export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWorkerPath = workerPath): void {
|
|
914
1003
|
pi.on("tool_result", async (event) => {
|
|
915
1004
|
if (!ORACLE_TOOL_NAMES.has(event.toolName as OracleToolName)) return;
|
|
916
1005
|
if (event.isError) return;
|
|
@@ -939,6 +1028,34 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
939
1028
|
},
|
|
940
1029
|
});
|
|
941
1030
|
|
|
1031
|
+
pi.registerTool({
|
|
1032
|
+
name: "oracle_auth",
|
|
1033
|
+
label: "Oracle Auth",
|
|
1034
|
+
description: "Refresh the shared oracle auth seed profile by importing ChatGPT cookies from your configured real Chrome profile.",
|
|
1035
|
+
promptSnippet: "Refresh oracle auth before retrying a login-required oracle run.",
|
|
1036
|
+
promptGuidelines: [
|
|
1037
|
+
"Call oracle_auth when an oracle run failed because ChatGPT login is required, the worker said to rerun /oracle-auth, or stale auth appears to be blocking submission execution.",
|
|
1038
|
+
"At most once per user request, refresh auth and then retry the blocked oracle submission.",
|
|
1039
|
+
"If oracle_auth itself fails, stop and report the failure instead of looping.",
|
|
1040
|
+
],
|
|
1041
|
+
parameters: Type.Object({}),
|
|
1042
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
1043
|
+
try {
|
|
1044
|
+
const projectCwd = getProjectId(ctx.cwd);
|
|
1045
|
+
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd);
|
|
1046
|
+
return {
|
|
1047
|
+
content: [{ type: "text" as const, text: message }],
|
|
1048
|
+
details: {
|
|
1049
|
+
refreshed: true,
|
|
1050
|
+
authSeedProfileDir: loadOracleConfig(projectCwd).browser.authSeedProfileDir,
|
|
1051
|
+
},
|
|
1052
|
+
};
|
|
1053
|
+
} catch (error) {
|
|
1054
|
+
return buildOracleToolErrorResult("oracle_auth", error, {});
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
|
|
942
1059
|
pi.registerTool({
|
|
943
1060
|
name: "oracle_submit",
|
|
944
1061
|
label: "Oracle Submit",
|
|
@@ -948,9 +1065,11 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
948
1065
|
promptSnippet: "Dispatch a background ChatGPT web oracle job after gathering repo context.",
|
|
949
1066
|
promptGuidelines: [
|
|
950
1067
|
"Gather context before calling oracle_submit.",
|
|
951
|
-
"
|
|
952
|
-
"
|
|
953
|
-
"
|
|
1068
|
+
"If the immediately preceding oracle run failed because ChatGPT login is required or the worker explicitly said to rerun /oracle-auth, call oracle_auth once before retrying the submission. Do not loop auth refreshes.",
|
|
1069
|
+
"Prefer context-rich archives up to the 250 MB ceiling because more relevant surrounding context is usually better than less.",
|
|
1070
|
+
"By default, archive the whole repo by passing '.' for broad or unclear requests; default archive exclusions apply automatically, including common bulky outputs and obvious credentials/private data like .env files, key material, credential dotfiles, local database files, and nested secrets directories anywhere in the repo.",
|
|
1071
|
+
"For narrower asks, still include nearby tests, docs, configs, and adjacent modules when they may improve answer quality. Only narrow aggressively when the user explicitly asks, privacy/sensitivity requires it, or size pressure forces it.",
|
|
1072
|
+
"Do not default to a one-file archive for a single function, file, or stack trace if the relevant surrounding context still fits comfortably within the limit.",
|
|
954
1073
|
"When files='.' and the post-exclusion archive is still too large, submit automatically prunes the largest nested directories matching generic generated-output names like build/, dist/, out/, coverage/, and tmp/ outside obvious source roots like src/ and lib/ until the archive fits or no candidate remains; successful submissions report what was pruned.",
|
|
955
1074
|
"If a submitted oracle job later fails because upload is rejected, retry smaller: remove the largest obviously irrelevant/generated content first, then narrow to modified files plus adjacent files plus directly relevant subtrees, then explain the cut or ask the user if still needed.",
|
|
956
1075
|
"If oracle_submit itself fails because the local archive still exceeds the upload limit after default exclusions and automatic generic generated-output-dir pruning, or for any other submit-time error, stop and report the error instead of retrying automatically.",
|
|
@@ -961,18 +1080,19 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
961
1080
|
parameters: ORACLE_SUBMIT_PARAMS,
|
|
962
1081
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
963
1082
|
try {
|
|
964
|
-
const
|
|
1083
|
+
const projectCwd = getProjectId(ctx.cwd);
|
|
1084
|
+
const config = loadOracleConfig(projectCwd);
|
|
965
1085
|
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
966
|
-
const projectId = getProjectId(
|
|
1086
|
+
const projectId = getProjectId(projectCwd);
|
|
967
1087
|
const sessionId = getSessionId(originSessionFile, projectId);
|
|
968
1088
|
const presetId = typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : config.defaults.preset;
|
|
969
1089
|
const selection = resolveOracleSubmitPreset(presetId);
|
|
970
|
-
const followUp = resolveFollowUp(params.followUpJobId,
|
|
1090
|
+
const followUp = resolveFollowUp(params.followUpJobId, projectCwd);
|
|
971
1091
|
// Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
|
|
972
|
-
resolveArchiveInputs(
|
|
1092
|
+
resolveArchiveInputs(projectCwd, params.files);
|
|
973
1093
|
await assertOracleSubmitPrerequisites(config);
|
|
974
1094
|
try {
|
|
975
|
-
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd:
|
|
1095
|
+
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: projectCwd }, async () => {
|
|
976
1096
|
await reconcileStaleOracleJobs();
|
|
977
1097
|
await pruneTerminalOracleJobs();
|
|
978
1098
|
});
|
|
@@ -993,7 +1113,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
993
1113
|
let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
|
|
994
1114
|
|
|
995
1115
|
try {
|
|
996
|
-
archive = await createArchive(
|
|
1116
|
+
archive = await createArchive(projectCwd, params.files, tempArchivePath);
|
|
997
1117
|
const currentArchive = archive;
|
|
998
1118
|
await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
|
|
999
1119
|
await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
|
|
@@ -1036,7 +1156,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
1036
1156
|
chatUrl: followUp.chatUrl,
|
|
1037
1157
|
requestSource: "tool",
|
|
1038
1158
|
},
|
|
1039
|
-
|
|
1159
|
+
projectCwd,
|
|
1040
1160
|
originSessionFile,
|
|
1041
1161
|
config,
|
|
1042
1162
|
runtime,
|
|
@@ -1079,7 +1199,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string): void
|
|
|
1079
1199
|
chatUrl: followUp.chatUrl,
|
|
1080
1200
|
requestSource: "tool",
|
|
1081
1201
|
},
|
|
1082
|
-
|
|
1202
|
+
projectCwd,
|
|
1083
1203
|
originSessionFile,
|
|
1084
1204
|
config,
|
|
1085
1205
|
runtime,
|
|
@@ -111,11 +111,11 @@ export function buildOracleWakeupNotificationContent(job, options = {}) {
|
|
|
111
111
|
const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
|
|
112
112
|
return [
|
|
113
113
|
`Oracle job ${job.id} is ${job.status}.`,
|
|
114
|
-
`Use
|
|
114
|
+
`Use /oracle-read ${job.id} to inspect the saved response preview. /oracle-status ${job.id} still shows saved job metadata. Agent callers can use oracle_read({ jobId: "${job.id}" }) if they need tool output in the current turn.`,
|
|
115
115
|
responseLine,
|
|
116
116
|
`Artifacts: ${artifactsPath}`,
|
|
117
117
|
formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
|
|
118
|
-
job.error ? `Error: ${job.error}` : "After
|
|
118
|
+
job.error ? `Error: ${job.error}` : "After opening the saved result, continue from the oracle output.",
|
|
119
119
|
].filter(Boolean).join("\n");
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -69,7 +69,7 @@ export function classifyChatAuthPage(args) {
|
|
|
69
69
|
const hasAddFiles = args.snapshot.includes(`button "${addFilesLabel}"`);
|
|
70
70
|
const hasModelControl =
|
|
71
71
|
args.snapshot.includes('button "Model selector"') ||
|
|
72
|
-
/button "(Instant|Thinking|Pro)(
|
|
72
|
+
/button "(?:Instant|(?:(?:Light|Standard|Extended|Heavy) )?Thinking|(?:(?:Light|Standard|Extended|Heavy) )?Pro)(?:, click to remove)?"/i.test(args.snapshot);
|
|
73
73
|
|
|
74
74
|
const challengePatterns = [
|
|
75
75
|
/just a moment/i,
|