pi-oracle 0.7.15 → 0.7.17
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 +24 -0
- package/README.md +7 -4
- package/docs/ORACLE_DESIGN.md +5 -2
- package/docs/ORACLE_RECOVERY_DRILL.md +2 -2
- package/extensions/oracle/index.ts +2 -5
- package/extensions/oracle/lib/commands.ts +36 -34
- package/extensions/oracle/lib/config.ts +37 -4
- package/extensions/oracle/lib/jobs.ts +4 -13
- package/extensions/oracle/lib/locks.ts +0 -8
- package/extensions/oracle/lib/poller.ts +1 -6
- package/extensions/oracle/lib/queue.ts +1 -12
- package/extensions/oracle/lib/runtime.ts +3 -4
- package/extensions/oracle/lib/tools.ts +56 -51
- package/extensions/oracle/lib/trust.ts +5 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +1 -10
- package/extensions/oracle/shared/state-path-helpers.d.mts +3 -0
- package/extensions/oracle/shared/state-path-helpers.mjs +9 -0
- package/extensions/oracle/shared/time-helpers.d.mts +1 -0
- package/extensions/oracle/shared/time-helpers.mjs +7 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +3 -33
- package/extensions/oracle/worker/run-job.mjs +10 -39
- package/package.json +8 -5
- package/prompts/oracle-followup.md +9 -2
- package/prompts/oracle.md +3 -2
- package/scripts/oracle-real-smoke.mjs +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.17 - 2026-07-01
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- clarified oracle prompt/docs validation around provider upload ceilings, Node release-gate requirements, and follow-up dispatch rules
|
|
9
|
+
- centralized provider normalization, project-trust probing, oracle jobs-dir defaults, and timestamp parsing in narrow source-of-truth modules
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- made sanity-runner mode and real-smoke timeout validation fail through tested behavior paths instead of brittle source-string contracts
|
|
13
|
+
- removed duplicate worker/auth helper logic for snapshot parsing, URL normalization, conversation ids, and login-probe normalization
|
|
14
|
+
|
|
15
|
+
### Validation
|
|
16
|
+
- ran the full `npm run verify:oracle` local gate before release
|
|
17
|
+
|
|
18
|
+
## 0.7.16 - 2026-06-24
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- updated the local pi development and validation baseline to `@earendil-works/*` `0.80.2`
|
|
22
|
+
- exposed packaged `/oracle` and `/oracle-followup` prompt templates through the package manifest so TUI slash completion can discover them
|
|
23
|
+
- reduced always-on `oracle_submit` prompt metadata while keeping detailed dispatch rules in the prompt templates
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- made `/oracle-*` command output visible in JSON mode through a displayed custom message instead of UI notifications only
|
|
27
|
+
- deferred Chrome user-agent probing until oracle config is loaded, with a timeout on the Chrome version probe
|
|
28
|
+
|
|
5
29
|
## 0.7.15 - 2026-06-23
|
|
6
30
|
|
|
7
31
|
### Changed
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`pi-oracle` lets a `pi` agent send hard, long-running work to ChatGPT.com or Grok through the web app, with repo archives, background execution, saved results, and a best-effort wake-up back into `pi` when the answer is ready.
|
|
4
4
|
|
|
5
|
-
> Status: experimental public beta.
|
|
5
|
+
> Status: experimental public beta. Current local validation baseline is pi `0.80.2`; the platform-smoke harness covers macOS, Linux, and Windows native with Chromium-family browsers. Pi `0.80.2+` is the suggested tested floor for project-trust-aware package/runtime validation, but pi-bundled runtime packages remain optional wildcard peers so npm peer ranges do not block users from trying newer pi releases. Normal oracle jobs run in an isolated browser profile, not your active browser window.
|
|
6
6
|
|
|
7
7
|
## What a successful run looks like
|
|
8
8
|
|
|
@@ -71,13 +71,15 @@ Or from GitHub:
|
|
|
71
71
|
pi install https://github.com/fitchmultz/pi-oracle
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
To update the package later, use `pi update --extensions`, `pi update --all`, or `pi update npm:pi-oracle`. Bare `pi update` updates pi itself only. Versioned npm/git refs stay pinned until you change the configured source.
|
|
75
|
+
|
|
74
76
|
### 2. Check requirements
|
|
75
77
|
|
|
76
78
|
You need:
|
|
77
79
|
|
|
78
80
|
- macOS, Linux, or Windows native
|
|
79
|
-
- Node.js 22 or newer
|
|
80
|
-
- Suggested tested floor: `pi` 0.80.
|
|
81
|
+
- Node.js 22.19.0 or newer for package install/use; platform smoke/release validation currently expects Node 24+ per `platform-smoke.config.mjs`
|
|
82
|
+
- Suggested tested floor: `pi` 0.80.2 or newer; older pi versions are not blocked by package metadata but are outside the current validation baseline
|
|
81
83
|
- Google Chrome/Chromium or another Chromium-family browser
|
|
82
84
|
- ChatGPT or Grok already signed in to the configured local browser profile for the provider you plan to use
|
|
83
85
|
- `agent-browser` and `tar` available on the machine; `zstd` is also required when submitting ChatGPT `.tar.zst` archives
|
|
@@ -389,7 +391,8 @@ Use the narrowest validation workflow that proves the change:
|
|
|
389
391
|
| Situation | Command(s) |
|
|
390
392
|
| --- | --- |
|
|
391
393
|
| Everyday local iteration | `npm run verify:oracle` |
|
|
392
|
-
| Platform-
|
|
394
|
+
| Platform-focused syntax/invariant sanity | `npm run check:platform-smoke`, `npm run sanity:oracle:platform` |
|
|
395
|
+
| Platform-sensitive runtime changes | `npm run smoke:platform:doctor`, then a focused `node scripts/platform-smoke.mjs run --target <target> --suite <suite>` |
|
|
393
396
|
| Platform matrix proof | `npm run smoke:platform:all` |
|
|
394
397
|
| ChatGPT preset release proof | `npm run release:proof:chatgpt-presets` |
|
|
395
398
|
| Publish/release gate | `npm run release:check` |
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -7,7 +7,7 @@ Companion doc:
|
|
|
7
7
|
- `docs/ORACLE_RECOVERY_DRILL.md` — safe expired-auth recovery validation drill
|
|
8
8
|
|
|
9
9
|
Compatibility target:
|
|
10
|
-
- `pi` 0.80.
|
|
10
|
+
- `pi` 0.80.2+ is the suggested tested floor for current project-trust-aware package/runtime validation
|
|
11
11
|
- package metadata keeps pi runtime packages as optional wildcard peers, so this suggested floor is not enforced as a hard npm install requirement
|
|
12
12
|
- current extension lifecycle only; no backward-compatibility shims for removed `session_switch` / `session_fork` events
|
|
13
13
|
|
|
@@ -115,7 +115,7 @@ It instructs the agent to:
|
|
|
115
115
|
2. stop right away if preflight reports the session or local oracle setup is not ready
|
|
116
116
|
3. understand whether the request is explicitly narrow or genuinely broad
|
|
117
117
|
4. if auth is missing, stale, or the worker explicitly said to rerun `/oracle-auth`, stop and tell the user to run `/oracle-auth` rather than launching auth automatically
|
|
118
|
-
5. gather enough repo context to submit well and bias toward context-rich archives when they fit within the provider ceiling: 250
|
|
118
|
+
5. gather enough repo context to submit well and bias toward context-rich archives when they fit within the provider ceiling: 250 MiB for ChatGPT and 200 MiB for Grok
|
|
119
119
|
6. if the request is narrow, start from the directly relevant area but still include nearby tests, docs, config, and adjacent modules when they may improve answer quality
|
|
120
120
|
7. if the request is broad/repo-wide, gather broader context and usually archive `.`
|
|
121
121
|
8. if `oracle_submit` fails before dispatch with an `archive_too_large` / upload-limit error, treat that as retryable: use the reported size summary plus any auto-pruned paths to cut scope and retry automatically with a smaller archive
|
|
@@ -639,6 +639,9 @@ Remaining non-blocking hardening work:
|
|
|
639
639
|
- keep hardening model-selection verification against future ChatGPT UI variation
|
|
640
640
|
|
|
641
641
|
Recent proof points:
|
|
642
|
+
- Pi 0.80.2 local gate: `npm run verify:oracle` passed on 2026-06-24 after the JSON command output, prompt-manifest, schema, and lazy Chrome-probe audit fixes
|
|
643
|
+
- Pi 0.80.2 isolated extension smokes: `.artifacts/real-smoke/run-1782321054924-jnq0x3` passed source proof, and `.artifacts/real-smoke/run-1782321056224-yuq5a2` passed packed-install proof
|
|
644
|
+
- Pi 0.80.2 JSON command smoke: `pi --no-extensions -e ./extensions/oracle/index.ts --mode json --no-session --no-approve "/oracle-status"` emitted displayed `oracle-command-output` JSON events
|
|
642
645
|
- Pi 0.79.10 local gate: `npm run verify:oracle` passed on 2026-06-22 after the 0.79.10 baseline refresh and `CONFIG_DIR_NAME` cleanup
|
|
643
646
|
- Pi 0.79.10 isolated extension smokes: `.artifacts/real-smoke/run-1782137209549-0xe67z` passed packed-install proof, and `.artifacts/real-smoke/run-1782137217821-95a1po` passed source model-agent proof
|
|
644
647
|
- Pi 0.79.10 platform artifacts: `.artifacts/platform-smoke/run-1782137574391-7lay68` (macOS platform-build), `.artifacts/platform-smoke/run-1782137619352-gku7jz` (macOS real-extension), `.artifacts/platform-smoke/run-1782137587082-d7kg4p` (Ubuntu platform-build), `.artifacts/platform-smoke/run-1782137619176-lgxezy` (Ubuntu real-extension), `.artifacts/platform-smoke/run-1782137625964-66z0oc` (Windows native platform-build), `.artifacts/platform-smoke/run-1782137752969-pbmdj1` (Windows native real-extension)
|
|
@@ -43,7 +43,7 @@ mkdir -p "$SEED"
|
|
|
43
43
|
chmod 700 "$SEED"
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
This simulates a seed profile with no usable
|
|
46
|
+
This simulates a seed profile with no usable provider session.
|
|
47
47
|
|
|
48
48
|
## Validation steps
|
|
49
49
|
|
|
@@ -72,7 +72,7 @@ Run:
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
Expected result:
|
|
75
|
-
-
|
|
75
|
+
- provider cookies are re-synced into the seed profile
|
|
76
76
|
- no real Chrome profile is mutated
|
|
77
77
|
- command reports success
|
|
78
78
|
|
|
@@ -11,6 +11,7 @@ import { loadOracleConfig } from "./lib/config.js";
|
|
|
11
11
|
import { registerOracleCommands } from "./lib/commands.js";
|
|
12
12
|
import { getSessionFile, pruneTerminalOracleJobs, reconcileStaleOracleJobs } from "./lib/jobs.js";
|
|
13
13
|
import { isLockTimeoutError, withGlobalReconcileLock } from "./lib/locks.js";
|
|
14
|
+
import { isOracleProjectTrusted } from "./lib/trust.js";
|
|
14
15
|
import { refreshOracleStatus, setOracleReadiness, startPoller, stopPoller } from "./lib/poller.js";
|
|
15
16
|
import { promoteQueuedJobs } from "./lib/queue.js";
|
|
16
17
|
import { assertOracleSubmitPrerequisites, hasPersistedSessionFile } from "./lib/runtime.js";
|
|
@@ -29,10 +30,6 @@ function oracleReadinessFromError(error: unknown): "auth_needed" | "config_error
|
|
|
29
30
|
return /auth seed profile/i.test(message) ? "auth_needed" : "config_error";
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
33
|
-
return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
33
|
function expandOraclePromptTemplate(source: string, args: string): string {
|
|
37
34
|
return source.replaceAll("$@", args).replaceAll("$ARGUMENTS", args);
|
|
38
35
|
}
|
|
@@ -94,7 +91,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
94
91
|
return;
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
const config = loadOracleConfig(ctx.cwd, { projectConfigTrusted:
|
|
94
|
+
const config = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isOracleProjectTrusted(ctx) });
|
|
98
95
|
setOracleReadiness(ctx, "loaded");
|
|
99
96
|
void assertOracleSubmitPrerequisites(config)
|
|
100
97
|
.then(() => setOracleReadiness(ctx, "ready"))
|
|
@@ -8,7 +8,8 @@ import { readFile } from "node:fs/promises";
|
|
|
8
8
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { formatOracleCancelOutcome, formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
|
|
10
10
|
import { runOracleAuthBootstrap } from "./auth.js";
|
|
11
|
-
import type
|
|
11
|
+
import { normalizeOracleProviderAlias, type OracleProvider } from "./config.js";
|
|
12
|
+
import { isOracleProjectTrusted } from "./trust.js";
|
|
12
13
|
import {
|
|
13
14
|
cancelOracleJob,
|
|
14
15
|
getJobDir,
|
|
@@ -27,11 +28,6 @@ import { refreshOracleStatus } from "./poller.js";
|
|
|
27
28
|
import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
|
|
28
29
|
import { getProjectId } from "./runtime.js";
|
|
29
30
|
|
|
30
|
-
export interface OracleCommandPromptTemplates {
|
|
31
|
-
oracle?: string;
|
|
32
|
-
oracleFollowup?: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
31
|
async function summarizeJob(jobId: string, options?: { responsePreview?: boolean }): Promise<string> {
|
|
36
32
|
const job = readJob(jobId);
|
|
37
33
|
if (!job) return `Oracle job ${jobId} not found.`;
|
|
@@ -72,37 +68,42 @@ function readScopedJob(jobId: string, cwd: string) {
|
|
|
72
68
|
}
|
|
73
69
|
|
|
74
70
|
function parseOracleAuthProvider(args: string): OracleProvider | undefined {
|
|
75
|
-
const value = args.trim()
|
|
71
|
+
const value = args.trim();
|
|
76
72
|
if (!value) return undefined;
|
|
77
|
-
|
|
78
|
-
if (
|
|
73
|
+
const provider = normalizeOracleProviderAlias(value);
|
|
74
|
+
if (provider) return provider;
|
|
79
75
|
throw new Error("Usage: /oracle-auth [chatgpt|grok]");
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
function
|
|
83
|
-
return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function emitCommandOutput(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
78
|
+
function emitCommandOutput(pi: ExtensionAPI, ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error" = "info"): void {
|
|
87
79
|
if (ctx.mode === "print") {
|
|
88
80
|
process.stdout.write(`${message}\n`);
|
|
89
81
|
return;
|
|
90
82
|
}
|
|
91
|
-
ctx.
|
|
83
|
+
if (ctx.hasUI) {
|
|
84
|
+
ctx.ui.notify(message, level);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
pi.sendMessage({
|
|
88
|
+
customType: "oracle-command-output",
|
|
89
|
+
content: message,
|
|
90
|
+
display: true,
|
|
91
|
+
details: { level },
|
|
92
|
+
});
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string
|
|
95
|
+
export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string): void {
|
|
95
96
|
pi.registerCommand("oracle-auth", {
|
|
96
97
|
description: "Sync ChatGPT or Grok cookies from the configured local browser profile into the provider auth seed profile",
|
|
97
98
|
handler: async (args, ctx) => {
|
|
98
99
|
try {
|
|
99
100
|
const provider = parseOracleAuthProvider(args);
|
|
100
101
|
const providerLabel = provider === "grok" ? "Grok" : provider === "chatgpt" ? "ChatGPT" : "configured provider";
|
|
101
|
-
emitCommandOutput(ctx, `Syncing ${providerLabel} cookies from the configured local browser profile into the oracle auth seed profile…`, "info");
|
|
102
|
-
const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd, provider, { projectConfigTrusted:
|
|
103
|
-
emitCommandOutput(ctx, result, "info");
|
|
102
|
+
emitCommandOutput(pi, ctx, `Syncing ${providerLabel} cookies from the configured local browser profile into the oracle auth seed profile…`, "info");
|
|
103
|
+
const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd, provider, { projectConfigTrusted: isOracleProjectTrusted(ctx) });
|
|
104
|
+
emitCommandOutput(pi, ctx, result, "info");
|
|
104
105
|
} catch (error) {
|
|
105
|
-
emitCommandOutput(ctx, error instanceof Error ? error.message : String(error), "warning");
|
|
106
|
+
emitCommandOutput(pi, ctx, error instanceof Error ? error.message : String(error), "warning");
|
|
106
107
|
}
|
|
107
108
|
},
|
|
108
109
|
});
|
|
@@ -113,12 +114,12 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
113
114
|
const explicitJobId = args.trim();
|
|
114
115
|
const jobId = explicitJobId || getLatestJobId(ctx.cwd);
|
|
115
116
|
if (!jobId) {
|
|
116
|
-
emitCommandOutput(ctx, "No oracle jobs found for this project", "info");
|
|
117
|
+
emitCommandOutput(pi, ctx, "No oracle jobs found for this project", "info");
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
120
|
const job = readScopedJob(jobId, ctx.cwd);
|
|
120
121
|
if (!job) {
|
|
121
|
-
emitCommandOutput(ctx, `Oracle job ${jobId} was not found in this project`, "warning");
|
|
122
|
+
emitCommandOutput(pi, ctx, `Oracle job ${jobId} was not found in this project`, "warning");
|
|
122
123
|
return;
|
|
123
124
|
}
|
|
124
125
|
if (isTerminalOracleJob(job)) {
|
|
@@ -130,7 +131,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
130
131
|
}
|
|
131
132
|
const summary = await summarizeJob(job.id);
|
|
132
133
|
const recentJobs = !explicitJobId ? listRecentJobIds(ctx.cwd) : undefined;
|
|
133
|
-
emitCommandOutput(ctx, [summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
|
|
134
|
+
emitCommandOutput(pi, ctx, [summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
|
|
134
135
|
},
|
|
135
136
|
});
|
|
136
137
|
|
|
@@ -140,12 +141,12 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
140
141
|
const explicitJobId = args.trim();
|
|
141
142
|
const jobId = explicitJobId || getLatestJobId(ctx.cwd);
|
|
142
143
|
if (!jobId) {
|
|
143
|
-
emitCommandOutput(ctx, "No oracle jobs found for this project", "info");
|
|
144
|
+
emitCommandOutput(pi, ctx, "No oracle jobs found for this project", "info");
|
|
144
145
|
return;
|
|
145
146
|
}
|
|
146
147
|
const job = readScopedJob(jobId, ctx.cwd);
|
|
147
148
|
if (!job) {
|
|
148
|
-
emitCommandOutput(ctx, `Oracle job ${jobId} was not found in this project`, "warning");
|
|
149
|
+
emitCommandOutput(pi, ctx, `Oracle job ${jobId} was not found in this project`, "warning");
|
|
149
150
|
return;
|
|
150
151
|
}
|
|
151
152
|
if (isTerminalOracleJob(job)) {
|
|
@@ -155,7 +156,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
155
156
|
cwd: ctx.cwd,
|
|
156
157
|
});
|
|
157
158
|
}
|
|
158
|
-
emitCommandOutput(ctx, await summarizeJob(job.id, { responsePreview: true }), "info");
|
|
159
|
+
emitCommandOutput(pi, ctx, await summarizeJob(job.id, { responsePreview: true }), "info");
|
|
159
160
|
},
|
|
160
161
|
});
|
|
161
162
|
|
|
@@ -164,20 +165,20 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
164
165
|
handler: async (args, ctx) => {
|
|
165
166
|
const jobId = args.trim();
|
|
166
167
|
if (!jobId) {
|
|
167
|
-
emitCommandOutput(ctx, "Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
|
|
168
|
+
emitCommandOutput(pi, ctx, "Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
|
|
168
169
|
return;
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
const job = readScopedJob(jobId, ctx.cwd);
|
|
172
173
|
if (!job) {
|
|
173
|
-
emitCommandOutput(ctx, `Oracle job ${jobId} not found in this project`, "warning");
|
|
174
|
+
emitCommandOutput(pi, ctx, `Oracle job ${jobId} not found in this project`, "warning");
|
|
174
175
|
return;
|
|
175
176
|
}
|
|
176
177
|
if (!isOpenOracleJob(job)) {
|
|
177
178
|
if (isTerminalOracleJob(job)) {
|
|
178
|
-
emitCommandOutput(ctx, `Job is already terminal: ${job.status}. Use /oracle-read ${job.id} for details or /oracle-clean ${job.id} to remove it.`, "info");
|
|
179
|
+
emitCommandOutput(pi, ctx, `Job is already terminal: ${job.status}. Use /oracle-read ${job.id} for details or /oracle-clean ${job.id} to remove it.`, "info");
|
|
179
180
|
} else {
|
|
180
|
-
emitCommandOutput(ctx, `Oracle job ${jobId} is not cancellable (${job.status})`, "info");
|
|
181
|
+
emitCommandOutput(pi, ctx, `Oracle job ${jobId} is not cancellable (${job.status})`, "info");
|
|
181
182
|
}
|
|
182
183
|
return;
|
|
183
184
|
}
|
|
@@ -187,7 +188,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
187
188
|
await promoteQueuedJobs({ workerPath, source: "oracle_cancel_command" });
|
|
188
189
|
}
|
|
189
190
|
refreshOracleStatus(ctx);
|
|
190
|
-
emitCommandOutput(ctx, formatOracleCancelOutcome(cancelled), "info");
|
|
191
|
+
emitCommandOutput(pi, ctx, formatOracleCancelOutcome(cancelled), "info");
|
|
191
192
|
},
|
|
192
193
|
});
|
|
193
194
|
|
|
@@ -196,19 +197,20 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
196
197
|
handler: async (args, ctx: ExtensionCommandContext) => {
|
|
197
198
|
const target = args.trim();
|
|
198
199
|
if (!target) {
|
|
199
|
-
emitCommandOutput(ctx, "Usage: /oracle-clean <job-id|all>", "warning");
|
|
200
|
+
emitCommandOutput(pi, ctx, "Usage: /oracle-clean <job-id|all>", "warning");
|
|
200
201
|
return;
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
const jobs = target === "all" ? listJobsForCwd(ctx.cwd) : [readScopedJob(target, ctx.cwd)].filter(Boolean);
|
|
204
205
|
if (jobs.length === 0) {
|
|
205
|
-
emitCommandOutput(ctx, "No matching oracle jobs found", "warning");
|
|
206
|
+
emitCommandOutput(pi, ctx, "No matching oracle jobs found", "warning");
|
|
206
207
|
return;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
const nonTerminalJobs = jobs.filter((job): job is NonNullable<typeof job> => Boolean(job && !isTerminalOracleJob(job)));
|
|
210
211
|
if (nonTerminalJobs.length > 0) {
|
|
211
212
|
emitCommandOutput(
|
|
213
|
+
pi,
|
|
212
214
|
ctx,
|
|
213
215
|
`Refusing to remove non-terminal oracle job${nonTerminalJobs.length === 1 ? "" : "s"}: ${nonTerminalJobs.map((job) => job.id).join(", ")}`,
|
|
214
216
|
"warning",
|
|
@@ -246,7 +248,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
246
248
|
: removedCount === jobs.length
|
|
247
249
|
? `Removed ${removedCount} oracle job director${removedCount === 1 ? "y" : "ies"}.`
|
|
248
250
|
: `Removed ${removedCount} of ${jobs.length} oracle job director${jobs.length === 1 ? "y" : "ies"}; retained ${jobs.length - removedCount} due to cleanup blockers or warnings.`;
|
|
249
|
-
emitCommandOutput(ctx, `${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
|
|
251
|
+
emitCommandOutput(pi, ctx, `${removalSummary}${warningSuffix}`, cleanupWarnings.length > 0 ? "warning" : "info");
|
|
250
252
|
},
|
|
251
253
|
});
|
|
252
254
|
}
|
|
@@ -22,6 +22,21 @@ import { getProjectId } from "./runtime.js";
|
|
|
22
22
|
export const ORACLE_PROVIDERS = ["chatgpt", "grok"] as const;
|
|
23
23
|
export type OracleProvider = (typeof ORACLE_PROVIDERS)[number];
|
|
24
24
|
|
|
25
|
+
export function normalizeOracleProviderAlias(value: string): OracleProvider | undefined {
|
|
26
|
+
const normalized = value.trim().toLowerCase();
|
|
27
|
+
if (normalized === "chatgpt" || normalized === "chat-gpt" || normalized === "openai") return "chatgpt";
|
|
28
|
+
if (normalized === "grok" || normalized === "xai" || normalized === "x.ai") return "grok";
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeOracleProvider(value: unknown, fallback: OracleProvider, toolName = "oracle_submit"): OracleProvider {
|
|
33
|
+
if (value === undefined) return fallback;
|
|
34
|
+
if (typeof value !== "string") throw new Error(`${toolName} provider must be a string`);
|
|
35
|
+
const provider = normalizeOracleProviderAlias(value);
|
|
36
|
+
if (provider) return provider;
|
|
37
|
+
throw new Error(`Unknown ${toolName} provider: ${value}. Use chatgpt or grok.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
export { resolveOracleArchiveFormat, resolveOracleProviderArchivePlan } from "./provider-capabilities.js";
|
|
26
41
|
export type { OracleArchiveFormat, OracleProviderArchivePlan } from "./provider-capabilities.js";
|
|
27
42
|
|
|
@@ -308,7 +323,7 @@ function detectDefaultChromeUserAgent(executablePath: string | undefined): strin
|
|
|
308
323
|
const platformToken = chromeUserAgentPlatformToken(process.platform);
|
|
309
324
|
if (!platformToken) return undefined;
|
|
310
325
|
try {
|
|
311
|
-
const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv() }).trim();
|
|
326
|
+
const versionOutput = execFileSync(executablePath, ["--version"], { encoding: "utf8", env: sweetCookieSafeStoragePasswordScrubbedEnv(), timeout: 1000 }).trim();
|
|
312
327
|
const versionMatch = versionOutput.match(/(\d+\.\d+\.\d+\.\d+)/);
|
|
313
328
|
if (!versionMatch) return undefined;
|
|
314
329
|
return `Mozilla/5.0 (${platformToken}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${versionMatch[1]} Safari/537.36`;
|
|
@@ -318,8 +333,17 @@ function detectDefaultChromeUserAgent(executablePath: string | undefined): strin
|
|
|
318
333
|
}
|
|
319
334
|
|
|
320
335
|
const detectedChromeExecutablePath = detectDefaultChromeExecutablePath();
|
|
321
|
-
|
|
336
|
+
let detectedChromeUserAgent: string | undefined;
|
|
337
|
+
let detectedChromeUserAgentResolved = false;
|
|
322
338
|
const agentExtensionsDir = join(getAgentDir(), "extensions");
|
|
339
|
+
|
|
340
|
+
function getDetectedChromeUserAgent(): string | undefined {
|
|
341
|
+
if (!detectedChromeUserAgentResolved) {
|
|
342
|
+
detectedChromeUserAgent = detectDefaultChromeUserAgent(detectedChromeExecutablePath);
|
|
343
|
+
detectedChromeUserAgentResolved = true;
|
|
344
|
+
}
|
|
345
|
+
return detectedChromeUserAgent;
|
|
346
|
+
}
|
|
323
347
|
const detectedChromeProfileName = detectDefaultBrowserProfileSource(process.platform);
|
|
324
348
|
|
|
325
349
|
export interface OracleConfigLoadOptions {
|
|
@@ -442,7 +466,7 @@ export const DEFAULT_CONFIG: OracleConfig = {
|
|
|
442
466
|
authUrl: "https://chatgpt.com/auth/login",
|
|
443
467
|
runMode: "headless",
|
|
444
468
|
executablePath: detectedChromeExecutablePath,
|
|
445
|
-
userAgent:
|
|
469
|
+
userAgent: undefined,
|
|
446
470
|
args: ["--disable-blink-features=AutomationControlled"],
|
|
447
471
|
},
|
|
448
472
|
auth: {
|
|
@@ -732,5 +756,14 @@ export function loadOracleConfig(cwd: string, options?: OracleConfigLoadOptions)
|
|
|
732
756
|
const details = getOracleConfigLoadDetails(cwd, options);
|
|
733
757
|
const globalConfig = readJson(details.agentConfigPath);
|
|
734
758
|
const projectConfig = details.projectConfigLoaded ? filterProjectConfig(readJson(details.projectConfigPath)) : undefined;
|
|
735
|
-
|
|
759
|
+
const globalBrowser = isObject(globalConfig) ? globalConfig.browser : undefined;
|
|
760
|
+
const hasConfiguredUserAgent = isObject(globalBrowser) && globalBrowser.userAgent !== undefined;
|
|
761
|
+
const defaults = {
|
|
762
|
+
...DEFAULT_CONFIG,
|
|
763
|
+
browser: {
|
|
764
|
+
...DEFAULT_CONFIG.browser,
|
|
765
|
+
userAgent: hasConfiguredUserAgent ? undefined : getDetectedChromeUserAgent(),
|
|
766
|
+
},
|
|
767
|
+
};
|
|
768
|
+
return validateOracleConfig(deepMerge(deepMerge(defaults, globalConfig), projectConfig));
|
|
736
769
|
}
|
|
@@ -30,6 +30,8 @@ import type { OracleJobLifecycleEvent as SharedOracleJobLifecycleEvent, OracleJo
|
|
|
30
30
|
import { hasDurableWorkerHandoff as sharedHasDurableWorkerHandoff } from "../shared/job-coordination-helpers.mjs";
|
|
31
31
|
import { isTrackedProcessAlive, readProcessStartedAt, spawnDetachedNodeProcess, terminateTrackedProcess } from "../shared/process-helpers.mjs";
|
|
32
32
|
import type { OracleConfig, OracleResolvedSelection } from "./config.js";
|
|
33
|
+
import { getOracleJobsDir } from "../shared/state-path-helpers.mjs";
|
|
34
|
+
import { parseTimestamp } from "../shared/time-helpers.mjs";
|
|
33
35
|
import { resolveOracleProviderArchivePlan } from "./provider-capabilities.js";
|
|
34
36
|
import { withJobLock, withLock } from "./locks.js";
|
|
35
37
|
import { cleanupRuntimeArtifacts, getProjectId, getSessionId, parseConversationId, requirePersistedSessionFile, type OracleCleanupReport } from "./runtime.js";
|
|
@@ -50,9 +52,8 @@ const ORACLE_JOB_DIR_RM_MAX_RETRIES = 5;
|
|
|
50
52
|
const ORACLE_JOB_DIR_RM_RETRY_DELAY_MS = 50;
|
|
51
53
|
const ORACLE_COMPLETE_JOB_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
52
54
|
const ORACLE_FAILED_JOB_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
53
|
-
export
|
|
54
|
-
|
|
55
|
-
const ORACLE_JOBS_DIR = process.env[ORACLE_JOBS_DIR_ENV]?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
55
|
+
export { DEFAULT_ORACLE_JOBS_DIR, ORACLE_JOBS_DIR_ENV, getOracleJobsDir } from "../shared/state-path-helpers.mjs";
|
|
56
|
+
const ORACLE_JOBS_DIR = getOracleJobsDir();
|
|
56
57
|
|
|
57
58
|
export function isActiveOracleJob(job: Pick<OracleJob, "status">): boolean {
|
|
58
59
|
return ACTIVE_ORACLE_JOB_STATUSES.includes(job.status);
|
|
@@ -217,10 +218,6 @@ export function getSessionFile(ctx: ExtensionContext): string | undefined {
|
|
|
217
218
|
return hasSessionFileAccessor(ctx.sessionManager) ? ctx.sessionManager.getSessionFile() : undefined;
|
|
218
219
|
}
|
|
219
220
|
|
|
220
|
-
export function getOracleJobsDir(): string {
|
|
221
|
-
return ORACLE_JOBS_DIR;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
221
|
export function getJobDir(id: string): string {
|
|
225
222
|
return join(ORACLE_JOBS_DIR, `oracle-${id}`);
|
|
226
223
|
}
|
|
@@ -297,12 +294,6 @@ export async function clearCleanupPending(jobId: string, at = new Date().toISOSt
|
|
|
297
294
|
}
|
|
298
295
|
}
|
|
299
296
|
|
|
300
|
-
function parseTimestamp(value: string | undefined): number | undefined {
|
|
301
|
-
if (!value) return undefined;
|
|
302
|
-
const parsed = Date.parse(value);
|
|
303
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
297
|
function notificationClaimIsOwnedBy(job: Pick<OracleJob, "notifyClaimedAt" | "notifyClaimedBy">, claimedBy: string, now = Date.now()): boolean {
|
|
307
298
|
if (job.notifyClaimedBy !== claimedBy) return false;
|
|
308
299
|
const claimedAtMs = parseTimestamp(job.notifyClaimedAt);
|
|
@@ -100,14 +100,6 @@ export async function withGlobalReconcileLock<T>(
|
|
|
100
100
|
return withLock("reconcile", "global", metadata, fn, { timeoutMs: options?.timeoutMs ?? 30_000 });
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export async function withGlobalScanLock<T>(
|
|
104
|
-
metadata: unknown,
|
|
105
|
-
fn: () => Promise<T>,
|
|
106
|
-
options?: { timeoutMs?: number },
|
|
107
|
-
): Promise<T> {
|
|
108
|
-
return withLock("scan", "global", metadata, fn, { timeoutMs: options?.timeoutMs ?? 5_000 });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
103
|
export async function withJobLock<T>(jobId: string, metadata: unknown, fn: () => Promise<T>): Promise<T> {
|
|
112
104
|
return withLock("job", jobId, metadata, fn, { timeoutMs: 10_000 });
|
|
113
105
|
}
|
|
@@ -7,6 +7,7 @@ import { existsSync } from "node:fs";
|
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { buildOracleStatusText, buildOracleWakeupNotificationContent, type OracleReadinessStatus } from "../shared/job-observability-helpers.mjs";
|
|
9
9
|
import { isProcessAlive, readProcessStartedAt } from "../shared/process-helpers.mjs";
|
|
10
|
+
import { parseTimestamp } from "../shared/time-helpers.mjs";
|
|
10
11
|
import { isLockTimeoutError, listLeaseMetadata, releaseLease, withGlobalReconcileLock, writeLeaseMetadata } from "./locks.js";
|
|
11
12
|
import {
|
|
12
13
|
getJobDir,
|
|
@@ -90,12 +91,6 @@ function jobMatchesContext(job: { projectId: string; sessionId: string }, sessio
|
|
|
90
91
|
return job.projectId === projectId && job.sessionId === sessionId;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
function parseTimestamp(value: string | undefined): number | undefined {
|
|
94
|
-
if (!value) return undefined;
|
|
95
|
-
const parsed = Date.parse(value);
|
|
96
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
94
|
function getWakeupTargetLeaseKey(sessionKey: string, processPid = process.pid, processStartedAt = readProcessStartedAt(process.pid) || "unknown"): string {
|
|
100
95
|
return `${sessionKey}::${processPid}::${processStartedAt}`;
|
|
101
96
|
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
15
15
|
import { loadOracleConfig } from "./config.js";
|
|
16
16
|
import { withLock } from "./locks.js";
|
|
17
|
-
import { appendCleanupWarnings,
|
|
17
|
+
import { appendCleanupWarnings, isTerminalOracleJob, listOracleJobDirs, readJob, spawnWorker, terminateWorkerPid, updateJob, type OracleJob } from "./jobs.js";
|
|
18
18
|
import { cleanupRuntimeArtifacts, releaseRuntimeLease, tryAcquireConversationLease, tryAcquireRuntimeLease } from "./runtime.js";
|
|
19
19
|
|
|
20
20
|
export interface OracleQueuePosition {
|
|
@@ -144,14 +144,3 @@ export async function promoteQueuedJobs(options: PromoteQueuedJobsOptions): Prom
|
|
|
144
144
|
return promoteQueuedJobsWithinAdmissionLock(options);
|
|
145
145
|
}, { timeoutMs: options.lockTimeoutMs });
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
export async function createQueuedJob(
|
|
149
|
-
id: string,
|
|
150
|
-
input: Parameters<typeof createJob>[1],
|
|
151
|
-
cwd: string,
|
|
152
|
-
originSessionFile: string | undefined,
|
|
153
|
-
config: Parameters<typeof createJob>[4],
|
|
154
|
-
runtime: Parameters<typeof createJob>[5],
|
|
155
|
-
): Promise<OracleJob> {
|
|
156
|
-
return createJob(id, input, cwd, originSessionFile, config, runtime, { initialState: "queued" });
|
|
157
|
-
}
|
|
@@ -13,12 +13,11 @@ import { assertNotKnownBrowserUserDataPath, sweetCookieSafeStoragePasswordScrubb
|
|
|
13
13
|
import { jobBlocksAdmission } from "../shared/job-coordination-helpers.mjs";
|
|
14
14
|
import { isTrackedProcessAlive } from "../shared/process-helpers.mjs";
|
|
15
15
|
import type { OracleConfig, OracleProvider } from "./config.js";
|
|
16
|
+
import { getOracleJobsDir } from "../shared/state-path-helpers.mjs";
|
|
16
17
|
import { resolveOracleProviderArchivePlan } from "./provider-capabilities.js";
|
|
17
18
|
import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withAuthLock } from "./locks.js";
|
|
18
19
|
|
|
19
20
|
const SEED_GENERATION_FILE = ".oracle-seed-generation";
|
|
20
|
-
const DEFAULT_ORACLE_JOBS_DIR = "/tmp";
|
|
21
|
-
const ORACLE_JOBS_DIR = process.env.PI_ORACLE_JOBS_DIR?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
22
21
|
const AGENT_BROWSER_BIN = [process.env.AGENT_BROWSER_PATH, "/opt/homebrew/bin/agent-browser", "/usr/local/bin/agent-browser"].find(
|
|
23
22
|
(candidate) => typeof candidate === "string" && candidate && existsSync(candidate),
|
|
24
23
|
) || "agent-browser";
|
|
@@ -338,7 +337,7 @@ export async function assertOracleSubmitPrerequisites(config: OracleConfig, prov
|
|
|
338
337
|
await assertRequiredLocalDependencyReady(dependency.name, dependency.command);
|
|
339
338
|
}
|
|
340
339
|
await assertWritableDirectory(config.browser.runtimeProfilesDir, "runtime profiles");
|
|
341
|
-
await assertWritableDirectory(
|
|
340
|
+
await assertWritableDirectory(getOracleJobsDir(), "jobs");
|
|
342
341
|
}
|
|
343
342
|
|
|
344
343
|
export function getSeedGeneration(config: OracleConfig): string | undefined {
|
|
@@ -359,7 +358,7 @@ export async function writeSeedGeneration(config: OracleConfig, value = new Date
|
|
|
359
358
|
}
|
|
360
359
|
|
|
361
360
|
function activeJobExists(jobId: string): boolean {
|
|
362
|
-
const path = join(
|
|
361
|
+
const path = join(getOracleJobsDir(), `oracle-${jobId}`, "job.json");
|
|
363
362
|
if (!existsSync(path)) return false;
|
|
364
363
|
try {
|
|
365
364
|
const job = JSON.parse(readFileSync(path, "utf8")) as {
|
|
@@ -10,13 +10,15 @@ import { join } from "node:path";
|
|
|
10
10
|
import { createArchive, type ArchiveCreationResult, type ArchiveSizeBreakdownRow } from "./archive.js";
|
|
11
11
|
import { runOracleAuthBootstrap } from "./auth.js";
|
|
12
12
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { Type } from "typebox";
|
|
13
|
+
import { Type, type Static } from "typebox";
|
|
14
14
|
import { formatOracleCancelOutcome, formatOracleJobSummary, formatOracleSubmitResponse } from "../shared/job-observability-helpers.mjs";
|
|
15
15
|
import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
16
16
|
import { isLockTimeoutError, withGlobalReconcileLock, withLock } from "./locks.js";
|
|
17
17
|
import {
|
|
18
18
|
coerceOracleSubmitPresetId,
|
|
19
19
|
loadOracleConfig,
|
|
20
|
+
normalizeOracleProvider,
|
|
21
|
+
GROK_MODES,
|
|
20
22
|
ORACLE_PROVIDERS,
|
|
21
23
|
ORACLE_SUBMIT_PRESET_IDS,
|
|
22
24
|
resolveOracleConfigForProvider,
|
|
@@ -48,6 +50,7 @@ import {
|
|
|
48
50
|
updateJob,
|
|
49
51
|
type OracleJob,
|
|
50
52
|
} from "./jobs.js";
|
|
53
|
+
import { isOracleProjectTrusted } from "./trust.js";
|
|
51
54
|
import { getQueuePosition, promoteQueuedJobs, promoteQueuedJobsWithinAdmissionLock } from "./queue.js";
|
|
52
55
|
import { resolveOracleProviderArchivePlan } from "./provider-capabilities.js";
|
|
53
56
|
import { refreshOracleStatus } from "./poller.js";
|
|
@@ -64,6 +67,16 @@ import {
|
|
|
64
67
|
tryAcquireRuntimeLease,
|
|
65
68
|
} from "./runtime.js";
|
|
66
69
|
|
|
70
|
+
const ORACLE_PROVIDER_PARAM_DESCRIPTION = `Oracle web provider. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}.`;
|
|
71
|
+
const ORACLE_PROVIDER_PARAM = Type.Optional(Type.String({
|
|
72
|
+
description: ORACLE_PROVIDER_PARAM_DESCRIPTION,
|
|
73
|
+
enum: [...ORACLE_PROVIDERS],
|
|
74
|
+
}));
|
|
75
|
+
const ORACLE_GROK_MODE_PARAM = Type.Optional(Type.String({
|
|
76
|
+
description: "Provider mode. For Grok, only heavy is currently supported. Omit to use the configured default mode.",
|
|
77
|
+
enum: [...GROK_MODES],
|
|
78
|
+
}));
|
|
79
|
+
|
|
67
80
|
const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
68
81
|
prompt: Type.String({ description: "Prompt text to send to ChatGPT or Grok web." }),
|
|
69
82
|
files: Type.Array(Type.String({
|
|
@@ -74,11 +87,7 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
|
74
87
|
description: "Exact project-relative files/directories to include in the oracle archive.",
|
|
75
88
|
minItems: 1,
|
|
76
89
|
}),
|
|
77
|
-
provider:
|
|
78
|
-
Type.String({
|
|
79
|
-
description: `Oracle web provider. Omit to use the configured default provider. Supported providers: ${ORACLE_PROVIDERS.join(", ")}. Use grok when the user asks to oracle to Grok. Grok archives are capped at 200 MiB.`,
|
|
80
|
-
}),
|
|
81
|
-
),
|
|
90
|
+
provider: ORACLE_PROVIDER_PARAM,
|
|
82
91
|
preset: Type.Optional(
|
|
83
92
|
Type.String({
|
|
84
93
|
description:
|
|
@@ -86,50 +95,55 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
|
|
|
86
95
|
"Matching human-readable preset labels and common hyphen/space variants are normalized automatically. Do not pass preset when provider is grok.",
|
|
87
96
|
}),
|
|
88
97
|
),
|
|
89
|
-
mode:
|
|
90
|
-
Type.String({
|
|
91
|
-
description: "Provider mode. For Grok, only heavy is currently supported. Omit to use the configured default mode.",
|
|
92
|
-
}),
|
|
93
|
-
),
|
|
98
|
+
mode: ORACLE_GROK_MODE_PARAM,
|
|
94
99
|
followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose chat thread should be continued." })),
|
|
95
100
|
chatGptConversationId: Type.Optional(Type.String({
|
|
96
101
|
description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, to continue. Omit for default behavior: starting a fresh oracle thread. Do not combine with followUpJobId.",
|
|
97
102
|
minLength: 1,
|
|
98
103
|
pattern: "^.*\\S.*$",
|
|
99
104
|
})),
|
|
100
|
-
});
|
|
105
|
+
}, { additionalProperties: false });
|
|
101
106
|
|
|
102
107
|
const ORACLE_PREFLIGHT_PARAMS = Type.Object({
|
|
103
|
-
provider:
|
|
108
|
+
provider: ORACLE_PROVIDER_PARAM,
|
|
104
109
|
followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose provider/thread readiness should be checked." })),
|
|
105
110
|
chatGptConversationId: Type.Optional(Type.String({
|
|
106
111
|
description: "Existing ChatGPT conversation id, or full https://chatgpt.com/c/... URL, whose provider/thread readiness should be checked. Do not combine with followUpJobId.",
|
|
107
112
|
minLength: 1,
|
|
108
113
|
pattern: "^.*\\S.*$",
|
|
109
114
|
})),
|
|
110
|
-
});
|
|
115
|
+
}, { additionalProperties: false });
|
|
111
116
|
|
|
112
117
|
const ORACLE_AUTH_PARAMS = Type.Object({
|
|
113
|
-
provider:
|
|
114
|
-
});
|
|
118
|
+
provider: ORACLE_PROVIDER_PARAM,
|
|
119
|
+
}, { additionalProperties: false });
|
|
115
120
|
|
|
116
121
|
const ORACLE_READ_PARAMS = Type.Object({
|
|
117
122
|
jobId: Type.String({ description: "Oracle job id." }),
|
|
118
|
-
});
|
|
123
|
+
}, { additionalProperties: false });
|
|
119
124
|
|
|
120
125
|
const ORACLE_CANCEL_PARAMS = Type.Object({
|
|
121
126
|
jobId: Type.String({ description: "Oracle job id." }),
|
|
122
|
-
});
|
|
127
|
+
}, { additionalProperties: false });
|
|
123
128
|
|
|
124
129
|
const MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME = 1;
|
|
125
130
|
const MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME = resolveOracleProviderArchivePlan("chatgpt").maxArchiveBytes;
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
function prepareOracleProviderAliases<T>(args: unknown, toolName: string): T {
|
|
132
|
+
const record = asRecord(args);
|
|
133
|
+
if (!record) return args as T;
|
|
134
|
+
return {
|
|
135
|
+
...record,
|
|
136
|
+
provider: typeof record.provider === "string" ? normalizeOracleProvider(record.provider, "chatgpt", toolName) : record.provider,
|
|
137
|
+
} as T;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function prepareOracleSubmitArguments(args: unknown): Static<typeof ORACLE_SUBMIT_PARAMS> {
|
|
141
|
+
const record = asRecord(args);
|
|
142
|
+
if (!record) return args as Static<typeof ORACLE_SUBMIT_PARAMS>;
|
|
143
|
+
return {
|
|
144
|
+
...prepareOracleProviderAliases<Record<string, unknown>>(record, "oracle_submit"),
|
|
145
|
+
mode: typeof record.mode === "string" ? normalizeGrokMode(record.mode, "heavy") : record.mode,
|
|
146
|
+
} as Static<typeof ORACLE_SUBMIT_PARAMS>;
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
function normalizeGrokMode(value: unknown, fallback: "heavy"): "heavy" {
|
|
@@ -691,10 +705,6 @@ function formatOraclePreflightResponse(details: OraclePreflightDetails): string
|
|
|
691
705
|
].filter(Boolean).join("\n");
|
|
692
706
|
}
|
|
693
707
|
|
|
694
|
-
function isProjectTrusted(ctx: ExtensionContext): boolean {
|
|
695
|
-
return (ctx as { isProjectTrusted?: () => boolean }).isProjectTrusted?.() ?? true;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
708
|
async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: unknown; followUpJobId?: unknown; chatGptConversationId?: unknown } = {}): Promise<OraclePreflightDetails> {
|
|
699
709
|
const sessionFile = getSessionFile(ctx);
|
|
700
710
|
if (!hasPersistedSessionFile(sessionFile)) {
|
|
@@ -722,7 +732,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
|
|
|
722
732
|
if (chatGptConversationId !== undefined && typeof chatGptConversationId !== "string") {
|
|
723
733
|
throw new Error("oracle_preflight chatGptConversationId must be a string");
|
|
724
734
|
}
|
|
725
|
-
const baseConfig = loadOracleConfig(ctx.cwd, { projectConfigTrusted:
|
|
735
|
+
const baseConfig = loadOracleConfig(ctx.cwd, { projectConfigTrusted: isOracleProjectTrusted(ctx) });
|
|
726
736
|
const target = resolveConversationTarget({ followUpJobId, chatGptConversationId, cwd: ctx.cwd, config: baseConfig });
|
|
727
737
|
provider = normalizeOracleProvider(params.provider, target.provider ?? baseConfig.defaults.provider, "oracle_preflight");
|
|
728
738
|
if (target.provider && provider !== target.provider) {
|
|
@@ -789,6 +799,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
789
799
|
"Call oracle_preflight before doing expensive /oracle preparation. Pass provider='grok' when the user explicitly asks for Grok, followUpJobId for same-thread follow-ups from oracle-created jobs, or chatGptConversationId when the user explicitly provides an existing ChatGPT browser conversation id/URL. If ready is false, stop immediately and report the suggested next step instead of reading files or crafting archive inputs.",
|
|
790
800
|
],
|
|
791
801
|
parameters: ORACLE_PREFLIGHT_PARAMS,
|
|
802
|
+
prepareArguments: (args) => prepareOracleProviderAliases(args, "oracle_preflight"),
|
|
792
803
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
793
804
|
const details = await runOraclePreflight(ctx, params);
|
|
794
805
|
return {
|
|
@@ -809,12 +820,13 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
809
820
|
"If oracle_auth itself fails, stop and report the failure instead of looping.",
|
|
810
821
|
],
|
|
811
822
|
parameters: ORACLE_AUTH_PARAMS,
|
|
823
|
+
prepareArguments: (args) => prepareOracleProviderAliases(args, "oracle_auth"),
|
|
812
824
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
813
825
|
try {
|
|
814
826
|
const projectCwd = getProjectId(ctx.cwd);
|
|
815
|
-
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted:
|
|
827
|
+
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isOracleProjectTrusted(ctx) });
|
|
816
828
|
const provider = normalizeOracleProvider(params.provider, baseConfig.defaults.provider, "oracle_auth");
|
|
817
|
-
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted:
|
|
829
|
+
const message = await runOracleAuthBootstrap(authWorkerPath, projectCwd, provider, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isOracleProjectTrusted(ctx) });
|
|
818
830
|
return {
|
|
819
831
|
content: [{ type: "text" as const, text: message }],
|
|
820
832
|
details: {
|
|
@@ -838,30 +850,20 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
838
850
|
"Optional thread target: pass `chatGptConversationId` only when the user explicitly provides an existing ChatGPT browser conversation id/URL to continue; omit it for the default fresh oracle thread.",
|
|
839
851
|
promptSnippet: "Dispatch a background ChatGPT or Grok web oracle job after gathering repo context.",
|
|
840
852
|
promptGuidelines: [
|
|
841
|
-
"
|
|
842
|
-
"
|
|
843
|
-
"
|
|
844
|
-
"
|
|
845
|
-
"
|
|
846
|
-
"
|
|
847
|
-
"
|
|
848
|
-
"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.",
|
|
849
|
-
"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.",
|
|
850
|
-
"If oracle_submit fails before dispatch with details.error.code === 'archive_too_large' or an upload-limit message, treat that as retryable and retry automatically with a smaller archive.",
|
|
851
|
-
"For archive_too_large retries, use the reported top-level size summary and any auto-pruned paths to decide what to cut: first remove the largest obviously irrelevant/generated/history/export content, then if needed narrow to the directly relevant subtrees plus adjacent docs/tests/config.",
|
|
852
|
-
"Do not loop forever: stop after at most two total oracle_submit attempts for the same request, and if it still does not fit explain what was cut and why.",
|
|
853
|
-
"For any other submit-time error, stop and report the error instead of retrying automatically.",
|
|
854
|
-
"If oracle_submit returns a queued job instead of an immediately dispatched one, treat that as success and stop exactly the same way.",
|
|
855
|
-
"After a successful or queued oracle_submit, stop; do not continue the task while the oracle job is running. If oracle_submit failed with retryable archive_too_large, narrow the archive and retry first.",
|
|
856
|
-
"For ChatGPT, use `preset` as the only model-selection parameter on oracle_submit. " +
|
|
857
|
-
`Canonical ids: ${ORACLE_SUBMIT_PRESET_IDS.join(", ")}. ` +
|
|
858
|
-
"matching human-readable preset labels are normalized automatically. Omit preset to use the configured default. For Grok, pass provider='grok' and omit preset; only Heavy is supported today.",
|
|
853
|
+
"Call oracle_preflight first, then gather enough context to choose archive inputs before oracle_submit.",
|
|
854
|
+
"Use context-rich archives when they fit; use files='.' for broad repo-wide asks. Default exclusions already skip common bulky outputs and obvious credentials/private data.",
|
|
855
|
+
"Pass chatGptConversationId only when the user explicitly provides an existing ChatGPT conversation id/URL; otherwise omit it for a fresh thread.",
|
|
856
|
+
"If auth is stale, call oracle_auth at most once before retrying. If oracle_auth fails, stop and report it.",
|
|
857
|
+
"If details.error.code is 'archive_too_large', retry once with a smaller archive using the reported size/pruning details; do not loop.",
|
|
858
|
+
"After a successful or queued oracle_submit, stop and report only the dispatch summary. Do not keep working while the oracle job runs.",
|
|
859
|
+
"For ChatGPT, use preset only when the user requests model control; otherwise omit it. For Grok, pass provider='grok' and omit preset.",
|
|
859
860
|
],
|
|
860
861
|
parameters: ORACLE_SUBMIT_PARAMS,
|
|
862
|
+
prepareArguments: prepareOracleSubmitArguments,
|
|
861
863
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
862
864
|
try {
|
|
863
865
|
const projectCwd = getProjectId(ctx.cwd);
|
|
864
|
-
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted:
|
|
866
|
+
const baseConfig = loadOracleConfig(projectCwd, { projectConfigTrustCwd: ctx.cwd, projectConfigTrusted: isOracleProjectTrusted(ctx) });
|
|
865
867
|
const originSessionFile = requirePersistedSessionFile(getSessionFile(ctx), "submit oracle jobs");
|
|
866
868
|
const projectId = getProjectId(projectCwd);
|
|
867
869
|
const sessionId = getSessionId(originSessionFile, projectId);
|
|
@@ -878,6 +880,9 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
|
|
|
878
880
|
if (provider === "grok" && typeof params.preset === "string") {
|
|
879
881
|
throw new Error("oracle_submit preset is only valid for ChatGPT. For Grok, use provider='grok' and mode='heavy'.");
|
|
880
882
|
}
|
|
883
|
+
if (provider === "chatgpt" && typeof params.mode === "string") {
|
|
884
|
+
throw new Error("oracle_submit mode is only valid for Grok. For ChatGPT, omit mode and use preset for model selection.");
|
|
885
|
+
}
|
|
881
886
|
const selection = provider === "grok"
|
|
882
887
|
? resolveOracleGrokMode(normalizeGrokMode(params.mode, baseConfig.defaults.grokMode))
|
|
883
888
|
: resolveOracleSubmitPreset(typeof params.preset === "string" ? coerceOracleSubmitPresetId(params.preset) : baseConfig.defaults.preset);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// Invariants/Assumptions: Job summaries read from durable job state, and lifecycle event trails are bounded and already normalized by shared lifecycle helpers.
|
|
6
6
|
|
|
7
7
|
import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent } from "./job-lifecycle-helpers.mjs";
|
|
8
|
+
import { parseTimestamp } from "./time-helpers.mjs";
|
|
8
9
|
|
|
9
10
|
/** @typedef {import("./job-observability-helpers.d.mts").OracleJobSummaryLike} OracleJobSummaryLike */
|
|
10
11
|
/** @typedef {import("./job-observability-helpers.d.mts").OracleJobSummaryOptions} OracleJobSummaryOptions */
|
|
@@ -68,16 +69,6 @@ const ACTIVE_SUMMARY_STATUSES = new Set(["preparing", "submitted", "waiting"]);
|
|
|
68
69
|
const DEFAULT_ORACLE_HEARTBEAT_STALE_MS = 3 * 60 * 1000;
|
|
69
70
|
const DEFAULT_ACTIVE_JOB_POLL_HINT_SECONDS = 15;
|
|
70
71
|
|
|
71
|
-
/**
|
|
72
|
-
* @param {string | undefined} value
|
|
73
|
-
* @returns {number | undefined}
|
|
74
|
-
*/
|
|
75
|
-
function parseTimestamp(value) {
|
|
76
|
-
if (!value) return undefined;
|
|
77
|
-
const ms = Date.parse(value);
|
|
78
|
-
return Number.isNaN(ms) ? undefined : ms;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
72
|
/**
|
|
82
73
|
* @param {number} elapsedMs
|
|
83
74
|
* @returns {string}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Purpose: Share oracle state path defaults across extension and detached workers.
|
|
2
|
+
// Scope: Path/env defaults only; no filesystem mutation.
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_ORACLE_JOBS_DIR = "/tmp";
|
|
5
|
+
export const ORACLE_JOBS_DIR_ENV = "PI_ORACLE_JOBS_DIR";
|
|
6
|
+
|
|
7
|
+
export function getOracleJobsDir(env = process.env) {
|
|
8
|
+
return env[ORACLE_JOBS_DIR_ENV]?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseTimestamp(value: string | undefined): number | undefined;
|
|
@@ -16,7 +16,9 @@ import {
|
|
|
16
16
|
} from "../shared/browser-profile-helpers.mjs";
|
|
17
17
|
import { ensureAccountCookie, filterImportableAuthCookies } from "./auth-cookie-policy.mjs";
|
|
18
18
|
import { getCookiesFromConfiguredChromiumSource } from "./chromium-cookie-source.mjs";
|
|
19
|
+
import { parseSnapshotEntries } from "./artifact-heuristics.mjs";
|
|
19
20
|
import { buildAllowedChatGptOrigins } from "./chatgpt-ui-helpers.mjs";
|
|
21
|
+
import { stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
|
|
20
22
|
import { buildAccountChooserCandidateLabels, classifyChatAuthPage, normalizeLoginProbeResult } from "./auth-flow-helpers.mjs";
|
|
21
23
|
|
|
22
24
|
const rawConfig = process.argv[2];
|
|
@@ -429,27 +431,6 @@ async function pageText() {
|
|
|
429
431
|
return stdout || "";
|
|
430
432
|
}
|
|
431
433
|
|
|
432
|
-
function parseSnapshotEntries(snapshot) {
|
|
433
|
-
return snapshot
|
|
434
|
-
.split("\n")
|
|
435
|
-
.map((line) => {
|
|
436
|
-
const refMatch = line.match(/\bref=(e\d+)\b/);
|
|
437
|
-
if (!refMatch) return undefined;
|
|
438
|
-
const kindMatch = line.match(/^\s*-\s*([^\s]+)/);
|
|
439
|
-
const quotedMatch = line.match(/"([^"]*)"/);
|
|
440
|
-
const valueMatch = line.match(/:\s*(.+)$/);
|
|
441
|
-
return {
|
|
442
|
-
line,
|
|
443
|
-
ref: `@${refMatch[1]}`,
|
|
444
|
-
kind: kindMatch ? kindMatch[1] : undefined,
|
|
445
|
-
label: quotedMatch ? quotedMatch[1] : undefined,
|
|
446
|
-
value: valueMatch ? valueMatch[1].trim() : undefined,
|
|
447
|
-
disabled: /\bdisabled\b/.test(line),
|
|
448
|
-
};
|
|
449
|
-
})
|
|
450
|
-
.filter(Boolean);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
434
|
function findEntry(snapshot, predicate) {
|
|
454
435
|
return parseSnapshotEntries(snapshot).find(predicate);
|
|
455
436
|
}
|
|
@@ -466,17 +447,6 @@ async function clickRef(ref, logLabel = `click ${ref}`) {
|
|
|
466
447
|
await targetCommand("click", ref, { logLabel });
|
|
467
448
|
}
|
|
468
449
|
|
|
469
|
-
function stripQuery(url) {
|
|
470
|
-
try {
|
|
471
|
-
const parsed = new URL(url);
|
|
472
|
-
parsed.hash = "";
|
|
473
|
-
parsed.search = "";
|
|
474
|
-
return parsed.toString();
|
|
475
|
-
} catch {
|
|
476
|
-
return url;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
450
|
function preferredProvider() {
|
|
481
451
|
return config?.defaults?.provider === "grok" ? "grok" : "chatgpt";
|
|
482
452
|
}
|
|
@@ -491,7 +461,7 @@ function providerName() {
|
|
|
491
461
|
|
|
492
462
|
function cookieOrigins() {
|
|
493
463
|
const providerOrigins = preferredProvider() === "grok" ? GROK_COOKIE_ORIGINS : CHATGPT_COOKIE_ORIGINS;
|
|
494
|
-
return Array.from(new Set([
|
|
464
|
+
return Array.from(new Set([stripUrlQueryAndHash(providerChatUrl()), ...providerOrigins]));
|
|
495
465
|
}
|
|
496
466
|
|
|
497
467
|
function cookieSource() {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
} from "../shared/job-coordination-helpers.mjs";
|
|
20
20
|
import { applyOracleJobCleanupWarnings, clearOracleJobCleanupState, transitionOracleJobPhase } from "../shared/job-lifecycle-helpers.mjs";
|
|
21
21
|
import { spawnDetachedNodeProcess, terminateTrackedProcess } from "../shared/process-helpers.mjs";
|
|
22
|
+
import { getOracleJobsDir } from "../shared/state-path-helpers.mjs";
|
|
22
23
|
import { extractArtifactLabels, FILE_LABEL_PATTERN_SOURCE, GENERIC_ARTIFACT_LABELS, parseSnapshotEntries, partitionStructuralArtifactCandidates } from "./artifact-heuristics.mjs";
|
|
23
24
|
import {
|
|
24
25
|
buildAllowedChatGptOrigins,
|
|
@@ -39,7 +40,8 @@ import {
|
|
|
39
40
|
autoSwitchToThinkingSelectionVisible,
|
|
40
41
|
stripChatGptResponseChrome,
|
|
41
42
|
} from "./chatgpt-ui-helpers.mjs";
|
|
42
|
-
import { assistantSnapshotSlice, nextStableValueState, providerSendAccepted, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
|
|
43
|
+
import { assistantSnapshotSlice, conversationIdFromUrl, nextStableValueState, providerSendAccepted, resolveStableConversationUrlCandidate, stripUrlQueryAndHash } from "./chatgpt-flow-helpers.mjs";
|
|
44
|
+
import { normalizeLoginProbeResult } from "./auth-flow-helpers.mjs";
|
|
43
45
|
import { assertNotKnownBrowserUserDataPath, scrubSweetCookieSafeStoragePasswordEnv, sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
|
|
44
46
|
import { createLease, listLeaseMetadata, readLeaseMetadata, releaseLease, withLock } from "./state-locks.mjs";
|
|
45
47
|
|
|
@@ -49,9 +51,7 @@ if (!jobId) {
|
|
|
49
51
|
process.exit(1);
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
const
|
|
53
|
-
const ORACLE_JOBS_DIR = process.env.PI_ORACLE_JOBS_DIR?.trim() || DEFAULT_ORACLE_JOBS_DIR;
|
|
54
|
-
const jobDir = join(ORACLE_JOBS_DIR, `oracle-${jobId}`);
|
|
54
|
+
const jobDir = join(getOracleJobsDir(), `oracle-${jobId}`);
|
|
55
55
|
const jobPath = `${jobDir}/job.json`;
|
|
56
56
|
const CHATGPT_LABELS = {
|
|
57
57
|
composer: "Chat with ChatGPT",
|
|
@@ -294,17 +294,6 @@ async function cpSupportsApfsClone() {
|
|
|
294
294
|
return cpSupportsApfsCloneFlag;
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
function parseConversationId(chatUrl) {
|
|
298
|
-
if (!chatUrl) return undefined;
|
|
299
|
-
try {
|
|
300
|
-
const parsed = new URL(chatUrl);
|
|
301
|
-
const match = parsed.pathname.match(/\/(?:c|chat)\/([^/?#]+)/i);
|
|
302
|
-
return match?.[1];
|
|
303
|
-
} catch {
|
|
304
|
-
return undefined;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
297
|
async function removeChromiumProcessSingletonArtifacts(profileDir) {
|
|
309
298
|
await Promise.all([
|
|
310
299
|
rm(join(profileDir, "SingletonLock"), { force: true }),
|
|
@@ -763,21 +752,7 @@ async function evalPage(job, script) {
|
|
|
763
752
|
}
|
|
764
753
|
|
|
765
754
|
async function loginProbe(job) {
|
|
766
|
-
|
|
767
|
-
if (!result || typeof result !== "object") {
|
|
768
|
-
return { ok: false, status: 0, error: "invalid-probe-result" };
|
|
769
|
-
}
|
|
770
|
-
return {
|
|
771
|
-
ok: result.ok === true,
|
|
772
|
-
status: typeof result.status === "number" ? result.status : 0,
|
|
773
|
-
pageUrl: typeof result.pageUrl === "string" ? result.pageUrl : undefined,
|
|
774
|
-
domLoginCta: result.domLoginCta === true,
|
|
775
|
-
onAuthPage: result.onAuthPage === true,
|
|
776
|
-
error: typeof result.error === "string" ? result.error : undefined,
|
|
777
|
-
bodyKeys: Array.isArray(result.bodyKeys) ? result.bodyKeys : [],
|
|
778
|
-
bodyHasId: result.bodyHasId === true,
|
|
779
|
-
bodyHasEmail: result.bodyHasEmail === true,
|
|
780
|
-
};
|
|
755
|
+
return normalizeLoginProbeResult(await evalPage(job, buildLoginProbeScript(5_000)));
|
|
781
756
|
}
|
|
782
757
|
|
|
783
758
|
async function currentUrl(job) {
|
|
@@ -785,10 +760,6 @@ async function currentUrl(job) {
|
|
|
785
760
|
return stdout;
|
|
786
761
|
}
|
|
787
762
|
|
|
788
|
-
function stripQuery(url) {
|
|
789
|
-
return stripUrlQueryAndHash(url);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
763
|
async function snapshotText(job) {
|
|
793
764
|
const { stdout } = await agentBrowser(job, "snapshot", "-i");
|
|
794
765
|
return stdout;
|
|
@@ -1829,7 +1800,7 @@ async function waitForStableChatUrl(job, previousChatUrl) {
|
|
|
1829
1800
|
await sleep(1000);
|
|
1830
1801
|
}
|
|
1831
1802
|
|
|
1832
|
-
return previousChatUrl ||
|
|
1803
|
+
return previousChatUrl || stripUrlQueryAndHash(await currentUrl(job));
|
|
1833
1804
|
}
|
|
1834
1805
|
|
|
1835
1806
|
async function waitForChatCompletion(job, baselineAssistantCount) {
|
|
@@ -2198,7 +2169,7 @@ async function waitForStableArtifactCandidates(job, responseIndex, responseText
|
|
|
2198
2169
|
}
|
|
2199
2170
|
|
|
2200
2171
|
async function reopenConversationForArtifacts(job, responseIndex, responseText, reason) {
|
|
2201
|
-
const targetUrl = job.chatUrl ||
|
|
2172
|
+
const targetUrl = job.chatUrl || stripUrlQueryAndHash(await currentUrl(job));
|
|
2202
2173
|
await log(`Reopening conversation before artifact capture (${reason}): ${targetUrl}`);
|
|
2203
2174
|
await agentBrowser(job, "open", targetUrl);
|
|
2204
2175
|
await agentBrowser(job, "wait", "1500");
|
|
@@ -2397,9 +2368,9 @@ async function run() {
|
|
|
2397
2368
|
await sleep(POST_SEND_SETTLE_MS);
|
|
2398
2369
|
|
|
2399
2370
|
const observedChatUrl = isGrokJob(currentJob)
|
|
2400
|
-
?
|
|
2371
|
+
? stripUrlQueryAndHash(await currentUrl(currentJob))
|
|
2401
2372
|
: await waitForStableChatUrl(currentJob, currentJob.chatUrl);
|
|
2402
|
-
const observedConversationId =
|
|
2373
|
+
const observedConversationId = conversationIdFromUrl(observedChatUrl) || currentJob.conversationId;
|
|
2403
2374
|
const awaitingResponsePatch = {
|
|
2404
2375
|
heartbeatAt: new Date().toISOString(),
|
|
2405
2376
|
...(observedConversationId ? { chatUrl: observedChatUrl, conversationId: observedConversationId } : {}),
|
|
@@ -2414,7 +2385,7 @@ async function run() {
|
|
|
2414
2385
|
const completion = await waitForChatCompletion(currentJob, baselineAssistantCount);
|
|
2415
2386
|
if (isGrokJob(currentJob) && !currentJob.conversationId) {
|
|
2416
2387
|
const stableGrokChatUrl = await waitForStableChatUrl(currentJob, undefined);
|
|
2417
|
-
const stableGrokConversationId =
|
|
2388
|
+
const stableGrokConversationId = conversationIdFromUrl(stableGrokChatUrl);
|
|
2418
2389
|
if (!stableGrokConversationId) {
|
|
2419
2390
|
throw new Error(`Grok response completed but the conversation URL did not stabilize; current URL: ${stableGrokChatUrl || "(unknown)"}`);
|
|
2420
2391
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-oracle",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,10 +42,13 @@
|
|
|
42
42
|
"pi": {
|
|
43
43
|
"extensions": [
|
|
44
44
|
"./extensions/oracle/index.ts"
|
|
45
|
+
],
|
|
46
|
+
"prompts": [
|
|
47
|
+
"./prompts"
|
|
45
48
|
]
|
|
46
49
|
},
|
|
47
50
|
"scripts": {
|
|
48
|
-
"check:oracle-extension": "node --check extensions/oracle/shared/browser-profile-helpers.mjs && node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-coding-agent --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
51
|
+
"check:oracle-extension": "node --check extensions/oracle/shared/browser-profile-helpers.mjs && node --check extensions/oracle/shared/process-helpers.mjs && node --check extensions/oracle/shared/state-path-helpers.mjs && node --check extensions/oracle/shared/time-helpers.mjs && node --check extensions/oracle/shared/state-coordination-helpers.mjs && node --check extensions/oracle/shared/job-coordination-helpers.mjs && node --check extensions/oracle/shared/job-lifecycle-helpers.mjs && node --check extensions/oracle/shared/job-observability-helpers.mjs && node --check extensions/oracle/worker/run-job.mjs && node --check extensions/oracle/worker/state-locks.mjs && node --check extensions/oracle/worker/artifact-heuristics.mjs && node --check extensions/oracle/worker/chatgpt-ui-helpers.mjs && node --check extensions/oracle/worker/chatgpt-flow-helpers.mjs && node --check extensions/oracle/worker/auth-flow-helpers.mjs && node --check extensions/oracle/worker/auth-cookie-policy.mjs && node --check extensions/oracle/worker/chromium-cookie-source.mjs && node --check extensions/oracle/worker/auth-bootstrap.mjs && esbuild extensions/oracle/index.ts --bundle --platform=node --format=esm --external:@earendil-works/pi-coding-agent --external:typebox --outfile=/tmp/pi-oracle-extension-check.js",
|
|
49
52
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
50
53
|
"typecheck:worker-helpers": "tsc --noEmit -p tsconfig.worker-helpers.json",
|
|
51
54
|
"sanity:oracle": "node scripts/oracle-sanity-runner.mjs",
|
|
@@ -63,7 +66,7 @@
|
|
|
63
66
|
"smoke:real": "npm run smoke:real:packed",
|
|
64
67
|
"smoke:real:doctor": "node scripts/oracle-real-smoke.mjs doctor",
|
|
65
68
|
"release:check": "npm run verify:oracle && npm run release:proof:chatgpt-presets && npm run smoke:platform:all",
|
|
66
|
-
"check:oracle-real-smoke": "node --check scripts/oracle-real-smoke.mjs",
|
|
69
|
+
"check:oracle-real-smoke": "node --check scripts/oracle-real-smoke.mjs && node --check scripts/oracle-sanity-runner.mjs",
|
|
67
70
|
"check:oracle-release-proof": "node --check scripts/oracle-chatgpt-preset-proof.mjs",
|
|
68
71
|
"release:proof:chatgpt-presets": "node scripts/oracle-chatgpt-preset-proof.mjs check",
|
|
69
72
|
"smoke:real:packed": "node scripts/oracle-real-smoke.mjs run --mode packed",
|
|
@@ -83,8 +86,8 @@
|
|
|
83
86
|
"protobufjs": "7.6.1"
|
|
84
87
|
},
|
|
85
88
|
"devDependencies": {
|
|
86
|
-
"@earendil-works/pi-ai": "^0.80.
|
|
87
|
-
"@earendil-works/pi-coding-agent": "^0.80.
|
|
89
|
+
"@earendil-works/pi-ai": "^0.80.2",
|
|
90
|
+
"@earendil-works/pi-coding-agent": "^0.80.2",
|
|
88
91
|
"@types/node": "^22.19.19",
|
|
89
92
|
"esbuild": "^0.28.0",
|
|
90
93
|
"tsx": "^4.22.3",
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Continue an earlier oracle job in the same provider thread
|
|
3
|
+
argument-hint: <job-id> <request>
|
|
3
4
|
---
|
|
4
5
|
You are preparing an `/oracle-followup` job.
|
|
5
6
|
|
|
6
7
|
Do not answer the user's request directly yet.
|
|
7
8
|
|
|
9
|
+
Hard requirements:
|
|
10
|
+
- Do not plan instead of submitting. The point of `/oracle-followup` is dispatch.
|
|
11
|
+
- Do not claim preflight, auth, archive prep, or submission happened unless the matching tool call actually happened in this turn.
|
|
12
|
+
- If a required tool call is unavailable or fails, stop and report that exact blocker instead of fabricating progress.
|
|
13
|
+
- After a successful or queued `oracle_submit`, your final answer must be only a terse dispatch summary with the job id and response path. Do not ask questions, offer to watch/poll/read, list next steps, or continue working.
|
|
14
|
+
|
|
8
15
|
Required workflow:
|
|
9
16
|
1. Parse the user request as `<job-id> <follow-up request>`.
|
|
10
17
|
2. If the request does not include both a prior oracle job id and a follow-up request, stop and report: `Usage: /oracle-followup <job-id> <request>. Find the job id in the earlier oracle response or via /oracle-status.`
|
|
@@ -13,7 +20,7 @@ Required workflow:
|
|
|
13
20
|
5. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
|
|
14
21
|
6. Treat the parsed job id as `followUpJobId` for `oracle_submit`.
|
|
15
22
|
7. Understand whether the follow-up request is explicitly narrow or genuinely broad.
|
|
16
|
-
8. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250
|
|
23
|
+
8. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MiB for ChatGPT, 200 MiB for Grok.
|
|
17
24
|
9. If the follow-up request is explicit and narrow, start from the directly relevant area but still include nearby files, tests, docs, configs, and adjacent modules when they may improve answer quality. Keep the archive tightly minimal only when the user explicitly asks for that, privacy/sensitivity requires it, or size pressure forces it.
|
|
18
25
|
10. If the follow-up request is broad, architectural, release-oriented, or otherwise repo-wide, gather broader context and usually archive `.`.
|
|
19
26
|
11. Choose archive inputs for the follow-up oracle job.
|
|
@@ -34,7 +41,7 @@ Rules:
|
|
|
34
41
|
- If the immediately preceding oracle run for this follow-up failed because ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, stop and tell the user to run `/oracle-auth` (or `/oracle-auth grok` for Grok). Do not call `oracle_auth` automatically from `/oracle-followup`.
|
|
35
42
|
- This prompt exists so normal users can continue the same provider thread without manually constructing `followUpJobId` tool calls.
|
|
36
43
|
- Always include an archive. Do not submit without context files.
|
|
37
|
-
- By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250
|
|
44
|
+
- By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250 MiB for ChatGPT and 200 MiB for Grok. For broad or unclear follow-up requests, include the whole repository by passing `.`. 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.
|
|
38
45
|
- Only limit file selection if the user explicitly requests a tight archive, if privacy/sensitivity requires it, or if the archive would otherwise exceed the size limit after exclusions/pruning.
|
|
39
46
|
- For targeted follow-ups, still include directly related surrounding files, tests, docs, configs, and adjacent modules when they may improve answer quality. Do not default to a one-file archive just because the user mentioned one file, one function, or one stack trace.
|
|
40
47
|
- Do not keep exploring once you already have enough context to submit well.
|
package/prompts/oracle.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Prepare and dispatch a ChatGPT or Grok web oracle job
|
|
3
|
+
argument-hint: <request>
|
|
3
4
|
---
|
|
4
5
|
You are preparing an /oracle job.
|
|
5
6
|
|
|
@@ -16,7 +17,7 @@ Required workflow:
|
|
|
16
17
|
1. Call `oracle_preflight` immediately. If the user says to use Grok, pass `provider: "grok"` to `oracle_preflight`. If the user says to use ChatGPT, pass `provider: "chatgpt"`. If the user says to use ChatGPT Instant, pass `provider: "chatgpt"` and later call `oracle_submit` with `preset: "instant"`. If the user explicitly provides an existing ChatGPT conversation id or `https://chatgpt.com/c/...` URL, pass it as `chatGptConversationId` to `oracle_preflight` and force `provider: "chatgpt"`. Omit `chatGptConversationId` unless the user explicitly asks to continue an existing browser-created ChatGPT thread.
|
|
17
18
|
2. If `oracle_preflight` reports `ready: false`, stop before any expensive prep. Do not read files, search the codebase, prepare archive inputs, or call `oracle_auth` automatically. Report the blocking issue plus the suggested next step.
|
|
18
19
|
3. Understand the request and decide whether it is explicitly narrow or genuinely broad.
|
|
19
|
-
4. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250
|
|
20
|
+
4. Gather enough repo context to choose archive inputs and write a strong oracle prompt. Bias toward context-rich submissions when they fit within the provider archive ceiling: 250 MiB for ChatGPT, 200 MiB for Grok.
|
|
20
21
|
5. If the user scope is explicit and narrow, start from the directly relevant area but still include nearby files, tests, docs, configs, and adjacent modules when they may improve answer quality. Keep the archive tightly minimal only when the user explicitly asks for that, privacy/sensitivity requires it, or size pressure forces it.
|
|
21
22
|
6. If the request is broad, architectural, release-oriented, or otherwise repo-wide, gather broader context and usually archive `.`.
|
|
22
23
|
7. Choose archive inputs for the oracle job.
|
|
@@ -39,7 +40,7 @@ Rules:
|
|
|
39
40
|
- Use `oracle_preflight` before any expensive `/oracle` preparation so missing persisted-session or local auth/config blockers fail fast.
|
|
40
41
|
- If ChatGPT/Grok login is required, the worker said to rerun `/oracle-auth`, or stale auth clearly blocked execution, stop and tell the user to run `/oracle-auth` (or `/oracle-auth grok` for Grok). Do not call `oracle_auth` automatically from `/oracle`.
|
|
41
42
|
- Always include an archive. Do not submit without context files.
|
|
42
|
-
- By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250
|
|
43
|
+
- By default, prefer context-rich archives up to the provider ceiling because more relevant context is usually better than less. The ceiling is 250 MiB for ChatGPT and 200 MiB for Grok. For broad or unclear requests, include the whole repository by passing `.`. 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.
|
|
43
44
|
- Only limit file selection if the user explicitly requests a tight archive, if privacy/sensitivity requires it, or if the archive would otherwise exceed the size limit after exclusions/pruning.
|
|
44
45
|
- For targeted asks, still include directly related surrounding files, tests, docs, configs, and adjacent modules when they may improve answer quality. Do not default to a one-file archive just because the user mentioned one file, one function, or one stack trace.
|
|
45
46
|
- Do not keep exploring once you already have enough context to submit well.
|
|
@@ -61,6 +61,12 @@ function truthy(value) {
|
|
|
61
61
|
return ["1", "true", "yes", "on"].includes(String(value ?? "").toLowerCase());
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
function positiveTimeoutMs(value, label) {
|
|
65
|
+
const timeoutMs = Number(value ?? DEFAULT_TIMEOUT_MS);
|
|
66
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error(`${label} must be a finite positive millisecond value`);
|
|
67
|
+
return timeoutMs;
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
function commandExists(command, args = ["--version"]) {
|
|
65
71
|
return new Promise((resolvePromise) => {
|
|
66
72
|
const child = spawn(command, args, { stdio: "ignore", shell: process.platform === "win32" });
|
|
@@ -130,7 +136,7 @@ async function doctor() {
|
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
function runCommand(command, args, options) {
|
|
133
|
-
const timeoutMs = options.timeoutMs
|
|
139
|
+
const timeoutMs = positiveTimeoutMs(options.timeoutMs, "command timeout");
|
|
134
140
|
return new Promise((resolvePromise) => {
|
|
135
141
|
const child = spawn(command, args, {
|
|
136
142
|
cwd: options.cwd,
|
|
@@ -375,7 +381,7 @@ async function runPiAgent({ prepared, agentDir, sessionDir, jobsDir, prompt, out
|
|
|
375
381
|
async function run(mode = "packed") {
|
|
376
382
|
const provider = env("PI_ORACLE_REAL_TEST_PROVIDER") ?? DEFAULT_PROVIDER;
|
|
377
383
|
const model = env("PI_ORACLE_REAL_TEST_MODEL") ?? DEFAULT_MODEL;
|
|
378
|
-
const timeoutMs =
|
|
384
|
+
const timeoutMs = positiveTimeoutMs(env("PI_ORACLE_REAL_TEST_TIMEOUT_MS"), "PI_ORACLE_REAL_TEST_TIMEOUT_MS");
|
|
379
385
|
const artifactRoot = resolve(env("PI_ORACLE_REAL_TEST_ARTIFACT_ROOT") ?? ".artifacts/real-smoke");
|
|
380
386
|
const runId = `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
381
387
|
const runDir = join(artifactRoot, runId);
|