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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.6.1 - 2026-04-13
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- whole-repo archive expansion now merges very large entry groups iteratively instead of using spread/flat patterns that can overflow the JavaScript call stack during `oracle_submit`
|
|
9
|
+
- oracle sanity coverage now guards the large-entry merge path so broad archive submissions regress to a real archive/env error instead of `Maximum call stack size exceeded`
|
|
10
|
+
|
|
11
|
+
## 0.6.0 - 2026-04-13
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `oracle_auth`, an agent-facing tool that mirrors `/oracle-auth` so oracle runs can refresh the shared ChatGPT auth seed profile before a single retry when stale auth blocks execution
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- `/oracle` and `/oracle-followup` now follow a stricter preflight-first flow, bias toward context-rich archive selection up to the 250 MB ceiling, and explicitly allow one `oracle_auth` refresh before retrying stale-auth/login-required failures
|
|
18
|
+
- package metadata now follows the current pi package dependency guidance by publishing `@mariozechner/pi-coding-agent` and `@sinclair/typebox` as peer dependencies while keeping local typechecking/dev resolution intact
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- `/oracle` no-session and missing-seed flows now stop before unnecessary repo exploration, and prompt-template guidance keeps relevant surrounding archive context instead of over-optimizing for minimal slices when extra context still fits
|
|
22
|
+
- oracle model selection now recognizes ChatGPT family controls exposed as radios/menu items plus durable closed-chip states like `Extended thinking` and `Extended Pro`, so remembered new-chat defaults no longer derail preset configuration or ready-state detection
|
|
23
|
+
- authenticated ready-state detection now accepts extended closed-chip model controls during auth/bootstrap verification, preventing false login/setup failures when ChatGPT remembers a non-default preset
|
|
24
|
+
|
|
3
25
|
## 0.5.0 - 2026-04-12
|
|
4
26
|
|
|
5
27
|
### Added
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ pi install https://github.com/fitchmultz/pi-oracle
|
|
|
52
52
|
|
|
53
53
|
The `/oracle` prompt now runs an early oracle preflight before it gathers repo context, so missing persisted-session or local auth/config blockers fail before the agent spends time reading files.
|
|
54
54
|
|
|
55
|
-
For explicitly narrow requests, `/oracle` should
|
|
55
|
+
For explicitly narrow requests, `/oracle` should still prefer a context-rich relevant archive up to the 250 MB ceiling, including nearby tests, docs, config, and adjacent modules when that can improve answer quality. Reserve tightly minimal archives for an explicit user request for a tight archive, privacy-sensitive material, or size-constrained cases. It should also omit `preset` and use the configured default model unless the task clearly needs a different one.
|
|
56
56
|
|
|
57
57
|
If you miss the wake-up, the result is still saved durably in the oracle job directory and can be read later.
|
|
58
58
|
|
|
@@ -67,14 +67,20 @@ If you miss the wake-up, the result is still saved durably in the oracle job dir
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
```text
|
|
70
|
-
/oracle Explain the README guidance for /oracle-clean retention grace.
|
|
70
|
+
/oracle Explain the README guidance for /oracle-clean retention grace. Archive README.md plus any nearby docs or implementation files that help answer accurately.
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
```text
|
|
74
|
+
/oracle-followup <job-id> Tighten the migration plan around rollback risk, and include the most relevant surrounding files/docs as long as the archive stays comfortably within the limit.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
After a job finishes, use `/oracle-followup <job-id> <request>` to continue the same ChatGPT thread without hand-writing the low-level `followUpJobId` tool parameter.
|
|
78
|
+
|
|
73
79
|
## High-level flow
|
|
74
80
|
|
|
75
81
|
```mermaid
|
|
76
82
|
flowchart LR
|
|
77
|
-
A["/oracle request"] --> B["Agent preflights, then gathers
|
|
83
|
+
A["/oracle request"] --> B["Agent preflights, then gathers a context-rich relevant repo slice"]
|
|
78
84
|
B --> C["oracle_submit builds archive"]
|
|
79
85
|
C --> D["Detached worker starts isolated ChatGPT runtime"]
|
|
80
86
|
D --> E["Archive + prompt sent to ChatGPT.com"]
|
|
@@ -88,13 +94,16 @@ If concurrency is full, the job is queued and starts automatically later.
|
|
|
88
94
|
|
|
89
95
|
User-facing commands:
|
|
90
96
|
- `/oracle <request>` — prompt template that tells the agent to gather context and dispatch an oracle job
|
|
97
|
+
- `/oracle-followup <job-id> <request>` — prompt template that continues an earlier oracle job in the same ChatGPT thread
|
|
91
98
|
- `/oracle-auth` — sync ChatGPT cookies from your real Chrome profile into the isolated oracle auth profile
|
|
92
|
-
- `/oracle-
|
|
93
|
-
- `/oracle-
|
|
99
|
+
- `/oracle-read [job-id]` — inspect job status plus the saved response preview
|
|
100
|
+
- `/oracle-status [job-id]` — inspect job status and list recent job ids when no explicit id is given
|
|
101
|
+
- `/oracle-cancel <job-id>` — cancel a queued or active job by id
|
|
94
102
|
- `/oracle-clean <job-id|all>` — remove temp files for terminal jobs; recently woken terminal jobs may stay retained briefly and return a retry-after hint
|
|
95
103
|
|
|
96
104
|
Agent-facing tools:
|
|
97
105
|
- `oracle_preflight`
|
|
106
|
+
- `oracle_auth`
|
|
98
107
|
- `oracle_submit`
|
|
99
108
|
- `oracle_read`
|
|
100
109
|
- `oracle_cancel`
|
|
@@ -152,7 +161,10 @@ Project config should only override safe, non-privileged settings.
|
|
|
152
161
|
- Jobs persist their response and any artifacts under `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` by default.
|
|
153
162
|
- Jobs can queue automatically if runtime capacity is full.
|
|
154
163
|
- Completion delivery into `pi` is best-effort wake-up based.
|
|
155
|
-
- If you miss the wake-up, use `
|
|
164
|
+
- If you miss the wake-up, use `/oracle-read [job-id]` to inspect the saved response preview.
|
|
165
|
+
- `/oracle-status [job-id]` still shows saved job metadata and lists recent job ids when you omit the id.
|
|
166
|
+
- Agent callers can use `oracle_read({ jobId })`.
|
|
167
|
+
- If a prior oracle run failed because ChatGPT login was required or the worker explicitly said to rerun `/oracle-auth`, agent callers can run `oracle_auth({})` once and then retry the submission once.
|
|
156
168
|
- `/oracle-clean` can still refuse a terminal job briefly after a wake-up send so saved response/artifact paths survive the follow-up turn; when that guard applies, it returns the next eligible cleanup time.
|
|
157
169
|
|
|
158
170
|
## Requirements
|
|
@@ -185,7 +197,9 @@ Project config should only override safe, non-privileged settings.
|
|
|
185
197
|
|
|
186
198
|
### A job finished but no wake-up arrived
|
|
187
199
|
|
|
188
|
-
- Use `/oracle-
|
|
200
|
+
- Use `/oracle-read [job-id]` to inspect the saved response preview.
|
|
201
|
+
- Use `/oracle-status [job-id]` when you want status metadata or need help finding a job id.
|
|
202
|
+
- Agent callers can use `oracle_read({ jobId })` if they need tool output in the current turn.
|
|
189
203
|
- Results are still saved on disk even if the reminder turn does not land.
|
|
190
204
|
|
|
191
205
|
### `/oracle-clean` refuses a terminal job right after completion
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -65,15 +65,21 @@ The extension now follows the current `pi` session lifecycle model:
|
|
|
65
65
|
- implemented as a prompt template, not an extension command
|
|
66
66
|
- asks the agent to gather context and dispatch an oracle job
|
|
67
67
|
- intentionally uses native pi prompt/template queueing so submissions survive streaming and compaction
|
|
68
|
+
- `/oracle-followup <job-id> <request>`
|
|
69
|
+
- implemented as a prompt template, not an extension command
|
|
70
|
+
- asks the agent to continue an earlier oracle job in the same ChatGPT thread via `followUpJobId`
|
|
71
|
+
- keeps same-thread continuation available to normal users without requiring raw tool-call syntax
|
|
68
72
|
|
|
69
73
|
### Commands
|
|
70
74
|
|
|
71
75
|
- `/oracle-auth`
|
|
72
76
|
- syncs ChatGPT cookies from the user’s real Chrome into the isolated oracle profile and verifies them there
|
|
77
|
+
- `/oracle-read [job-id]`
|
|
78
|
+
- shows job status plus the saved response preview
|
|
73
79
|
- `/oracle-status [job-id]`
|
|
74
|
-
- shows job status
|
|
75
|
-
- `/oracle-cancel
|
|
76
|
-
- cancels a queued or active job
|
|
80
|
+
- shows job status and lists recent job ids when the caller omits an explicit id
|
|
81
|
+
- `/oracle-cancel <job-id>`
|
|
82
|
+
- cancels a queued or active job by id; does not guess a default target
|
|
77
83
|
- `/oracle-clean <job-id|all>`
|
|
78
84
|
- removes temp files for terminal jobs only
|
|
79
85
|
|
|
@@ -82,6 +88,8 @@ The extension now follows the current `pi` session lifecycle model:
|
|
|
82
88
|
- `oracle_preflight`
|
|
83
89
|
- lightweight agent-facing readiness check for persisted-session and local oracle prerequisites
|
|
84
90
|
- intended to run before expensive `/oracle` context gathering
|
|
91
|
+
- `oracle_auth`
|
|
92
|
+
- agent-facing auth refresh tool that mirrors `/oracle-auth` for stale-auth recovery before a retry
|
|
85
93
|
- `oracle_submit`
|
|
86
94
|
- low-level agent-facing dispatch tool
|
|
87
95
|
- creates archive and launches a detached worker
|
|
@@ -103,12 +111,13 @@ Instead it instructs the agent to:
|
|
|
103
111
|
1. call `oracle_preflight` immediately
|
|
104
112
|
2. stop right away if preflight reports the session or local oracle setup is not ready
|
|
105
113
|
3. understand whether the request is explicitly narrow or genuinely broad
|
|
106
|
-
4.
|
|
107
|
-
5.
|
|
108
|
-
6. if the request is
|
|
109
|
-
7.
|
|
110
|
-
8.
|
|
111
|
-
9.
|
|
114
|
+
4. 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
|
|
115
|
+
5. gather enough repo context to submit well and bias toward context-rich archives when they fit within the 250 MB ceiling
|
|
116
|
+
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
|
|
117
|
+
7. if the request is broad/repo-wide, gather broader context and usually archive `.`
|
|
118
|
+
8. craft the oracle prompt
|
|
119
|
+
9. call `oracle_submit`
|
|
120
|
+
10. stop and wait for the completion wake-up (best-effort; durable oracle response/artifact state is already persisted outside session history)
|
|
112
121
|
|
|
113
122
|
### `/oracle-auth`
|
|
114
123
|
|
|
@@ -139,7 +148,7 @@ The authenticated seed profile remains the source of truth for future oracle run
|
|
|
139
148
|
|
|
140
149
|
### `oracle_submit`
|
|
141
150
|
|
|
142
|
-
Agent-facing submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. **`preset` is the only model-selection parameter** on `oracle_submit`. There are no `modelFamily`, `effort`, or `autoSwitchToThinking` fields. Submit-time inputs accept canonical preset ids plus matching human-readable labels/common hyphen-space variants, and the tool normalizes them back to the canonical id before persisting job state. Prompt-template guidance biases toward omitting `preset` and using the configured default unless the task clearly needs a different model or the user explicitly asked for one.
|
|
151
|
+
Agent-facing submissions use **`preset`**; the canonical registry is `ORACLE_SUBMIT_PRESETS` in `extensions/oracle/lib/config.ts`. **`preset` is the only model-selection parameter** on `oracle_submit`. There are no `modelFamily`, `effort`, or `autoSwitchToThinking` fields. Submit-time inputs accept canonical preset ids plus matching human-readable labels/common hyphen-space variants, and the tool normalizes them back to the canonical id before persisting job state. Prompt-template guidance biases toward omitting `preset` and using the configured default unless the task clearly needs a different model or the user explicitly asked for one. It also biases toward context-rich archives up to the 250 MB ceiling, narrowing only when the user explicitly asks for a tight archive, privacy/sensitivity requires it, or size pressure forces it.
|
|
143
152
|
|
|
144
153
|
1. resolve the preset (submit-time or config default) into an execution snapshot
|
|
145
154
|
2. resolve optional `followUpJobId` into a prior `chatUrl` and `conversationId`
|
|
@@ -454,6 +463,7 @@ Same-thread continuity is persisted as data, not runtime browser state.
|
|
|
454
463
|
|
|
455
464
|
Approach:
|
|
456
465
|
|
|
466
|
+
- expose `/oracle-followup <job-id> <request>` as the user-facing way to continue the same ChatGPT thread later
|
|
457
467
|
- store `chatUrl` only after the conversation URL stabilizes
|
|
458
468
|
- derive and persist `conversationId` from that URL when possible
|
|
459
469
|
- for a follow-up job, resolve `followUpJobId` to the prior `chatUrl`
|
|
@@ -473,10 +483,10 @@ The extension still uses the same general `pi`-native background completion patt
|
|
|
473
483
|
- poller scans jobs on an interval
|
|
474
484
|
- completed job durability lives in oracle job state plus saved response/artifact files, not in synthetic session-history assistant messages
|
|
475
485
|
- when a matching job reaches `complete`, `failed`, or `cancelled`, the poller issues bounded best-effort wake-up reminders to whichever matching session is currently live
|
|
476
|
-
- those wake-ups direct the receiver to `
|
|
477
|
-
- manual `oracle_read
|
|
486
|
+
- those wake-ups direct the receiver to `/oracle-read [job-id]` as the primary completion-consumption path, while still surfacing saved response/artifact paths as secondary context; `/oracle-status` remains useful for metadata and job-id discovery, and agent callers can still use `oracle_read` when they need tool output in-turn
|
|
487
|
+
- manual `oracle_read`, `/oracle-read`, or `/oracle-status` inspection settles further reminder retries once the terminal job has been opened and persists provenance about which path/session settled the wake-up
|
|
478
488
|
- manual inspection before the first wake-up attempt is recorded separately as observation metadata and does not suppress the first reminder send
|
|
479
|
-
- if no wake-up lands, the job remains available via `/oracle-status`, `oracle_read`, and the saved `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` response/artifact files
|
|
489
|
+
- if no wake-up lands, the job remains available via `/oracle-read`, `/oracle-status`, `oracle_read`, and the saved `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` response/artifact files
|
|
480
490
|
- because completion delivery is best-effort, pruning uses explicit terminal-job age policy instead of pretending a durable session notification happened
|
|
481
491
|
- recently sent wake-ups keep response/artifact files retained briefly so follow-up turns do not point at deleted paths if cleanup or pruning races with delivery
|
|
482
492
|
|
|
@@ -518,8 +528,8 @@ Implemented in code for the pivot and concurrency redesign:
|
|
|
518
528
|
|
|
519
529
|
Retained from the earlier MVP:
|
|
520
530
|
|
|
521
|
-
- `/oracle`, `/oracle-status`, `/oracle-cancel`, `/oracle-clean`
|
|
522
|
-
- `oracle_submit`, `oracle_read`, `oracle_cancel`
|
|
531
|
+
- `/oracle`, `/oracle-followup`, `/oracle-read`, `/oracle-status`, `/oracle-cancel`, `/oracle-clean`
|
|
532
|
+
- `oracle_auth`, `oracle_submit`, `oracle_read`, `oracle_cancel`
|
|
523
533
|
- detached background worker model
|
|
524
534
|
- `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/...` state layout
|
|
525
535
|
- shell-safe archive creation using `tar` piped to `zstd`
|
|
@@ -21,7 +21,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
21
21
|
const authWorkerPath = join(extensionDir, "worker", "auth-bootstrap.mjs");
|
|
22
22
|
|
|
23
23
|
registerOracleCommands(pi, authWorkerPath, workerPath);
|
|
24
|
-
registerOracleTools(pi, workerPath);
|
|
24
|
+
registerOracleTools(pi, workerPath, authWorkerPath);
|
|
25
25
|
|
|
26
26
|
async function runStartupMaintenance(ctx: ExtensionContext): Promise<void> {
|
|
27
27
|
try {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Purpose: Share oracle auth-bootstrap orchestration between slash commands and agent-facing tools.
|
|
2
|
+
// Responsibilities: Load effective auth guidance, run reconcile maintenance, spawn the auth bootstrap worker, and return user-facing results.
|
|
3
|
+
// Scope: Extension-side auth bootstrap orchestration only; browser cookie import and profile validation stay in worker/auth-bootstrap.mjs.
|
|
4
|
+
// Usage: Imported by oracle commands and tools whenever the shared oracle auth seed profile must be refreshed.
|
|
5
|
+
// Invariants/Assumptions: Auth bootstrap runs under the global reconcile lock when available, uses the effective oracle config for the current workspace root, and returns the worker's stdout/stderr message verbatim on success or failure.
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { formatOracleAuthConfigRemediation, formatOracleAuthConfigSummary, getOracleConfigLoadDetails, loadOracleConfig } from "./config.js";
|
|
8
|
+
import { pruneTerminalOracleJobs, reconcileStaleOracleJobs } from "./jobs.js";
|
|
9
|
+
import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
|
|
10
|
+
|
|
11
|
+
export async function runOracleAuthBootstrap(authWorkerPath: string, cwd: string): Promise<string> {
|
|
12
|
+
const config = loadOracleConfig(cwd);
|
|
13
|
+
const configLoad = getOracleConfigLoadDetails(cwd);
|
|
14
|
+
const authConfigGuidance = {
|
|
15
|
+
...configLoad,
|
|
16
|
+
remediation: formatOracleAuthConfigRemediation(configLoad),
|
|
17
|
+
summary: formatOracleAuthConfigSummary(configLoad),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_auth", cwd }, async () => {
|
|
22
|
+
await reconcileStaleOracleJobs();
|
|
23
|
+
await pruneTerminalOracleJobs();
|
|
24
|
+
});
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!isLockTimeoutError(error, "reconcile", "global")) throw error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return await new Promise<string>((resolve, reject) => {
|
|
30
|
+
const child = spawn(process.execPath, [authWorkerPath, JSON.stringify({ config, configLoad: authConfigGuidance })], {
|
|
31
|
+
cwd,
|
|
32
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let stdout = "";
|
|
36
|
+
let stderr = "";
|
|
37
|
+
child.stdout.on("data", (data) => {
|
|
38
|
+
stdout += String(data);
|
|
39
|
+
});
|
|
40
|
+
child.stderr.on("data", (data) => {
|
|
41
|
+
stderr += String(data);
|
|
42
|
+
});
|
|
43
|
+
child.on("error", (error) => reject(error));
|
|
44
|
+
child.on("close", (code) => {
|
|
45
|
+
const message = stdout.trim() || stderr.trim() || "Oracle auth bootstrap finished with no output.";
|
|
46
|
+
if (code === 0) resolve(message);
|
|
47
|
+
else reject(new Error(message));
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
// Scope: Command-facing orchestration only; durable lifecycle mutations live in jobs/runtime/tools modules and browser execution stays in worker scripts.
|
|
4
4
|
// Usage: Imported by the oracle extension entrypoint to register /oracle-* commands with pi.
|
|
5
5
|
// Invariants/Assumptions: Commands operate on persisted project-scoped jobs and rely on shared observability formatting for detached-state clarity.
|
|
6
|
-
import { spawn } from "node:child_process";
|
|
7
6
|
import { existsSync } from "node:fs";
|
|
7
|
+
import { readFile } from "node:fs/promises";
|
|
8
8
|
import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
import { formatOracleJobSummary } from "../shared/job-observability-helpers.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { runOracleAuthBootstrap } from "./auth.js";
|
|
11
11
|
import {
|
|
12
12
|
cancelOracleJob,
|
|
13
13
|
getJobDir,
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
isTerminalOracleJob,
|
|
16
16
|
listJobsForCwd,
|
|
17
17
|
markWakeupSettled,
|
|
18
|
-
pruneTerminalOracleJobs,
|
|
19
18
|
readJob,
|
|
20
19
|
reconcileStaleOracleJobs,
|
|
21
20
|
removeTerminalOracleJob,
|
|
@@ -26,15 +25,25 @@ import { refreshOracleStatus } from "./poller.js";
|
|
|
26
25
|
import { isLockTimeoutError, withGlobalReconcileLock } from "./locks.js";
|
|
27
26
|
import { getProjectId } from "./runtime.js";
|
|
28
27
|
|
|
29
|
-
function summarizeJob(jobId: string): string {
|
|
28
|
+
async function summarizeJob(jobId: string, options?: { responsePreview?: boolean }): Promise<string> {
|
|
30
29
|
const job = readJob(jobId);
|
|
31
30
|
if (!job) return `Oracle job ${jobId} not found.`;
|
|
32
31
|
|
|
33
32
|
const responseAvailable = Boolean(job.responsePath && existsSync(job.responsePath));
|
|
33
|
+
let responsePreview: string | undefined;
|
|
34
|
+
if (options?.responsePreview && responseAvailable && job.responsePath) {
|
|
35
|
+
try {
|
|
36
|
+
responsePreview = (await readFile(job.responsePath, "utf8")).slice(0, 4000);
|
|
37
|
+
} catch {
|
|
38
|
+
responsePreview = undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
return formatOracleJobSummary(job, {
|
|
35
43
|
queuePosition: job.status === "queued" ? getQueuePosition(job.id) : undefined,
|
|
36
44
|
artifactsPath: `${getJobDir(job.id)}/artifacts`,
|
|
37
45
|
responseAvailable,
|
|
46
|
+
responsePreview,
|
|
38
47
|
});
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -42,59 +51,25 @@ function getLatestJobId(cwd: string): string | undefined {
|
|
|
42
51
|
return listJobsForCwd(cwd)[0]?.id;
|
|
43
52
|
}
|
|
44
53
|
|
|
54
|
+
function listRecentJobIds(cwd: string, limit = 5): string | undefined {
|
|
55
|
+
const jobs = listJobsForCwd(cwd).slice(0, limit);
|
|
56
|
+
if (jobs.length === 0) return undefined;
|
|
57
|
+
return jobs.map((job) => `${job.id} (${job.status})`).join(", ");
|
|
58
|
+
}
|
|
59
|
+
|
|
45
60
|
function readScopedJob(jobId: string, cwd: string) {
|
|
46
61
|
const job = readJob(jobId);
|
|
47
62
|
if (!job || job.projectId !== getProjectId(cwd)) return undefined;
|
|
48
63
|
return job;
|
|
49
64
|
}
|
|
50
65
|
|
|
51
|
-
async function runAuthBootstrap(authWorkerPath: string, cwd: string): Promise<string> {
|
|
52
|
-
const config = loadOracleConfig(cwd);
|
|
53
|
-
const configLoad = getOracleConfigLoadDetails(cwd);
|
|
54
|
-
const authConfigGuidance = {
|
|
55
|
-
...configLoad,
|
|
56
|
-
remediation: formatOracleAuthConfigRemediation(configLoad),
|
|
57
|
-
summary: formatOracleAuthConfigSummary(configLoad),
|
|
58
|
-
};
|
|
59
|
-
try {
|
|
60
|
-
await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_auth", cwd }, async () => {
|
|
61
|
-
await reconcileStaleOracleJobs();
|
|
62
|
-
await pruneTerminalOracleJobs();
|
|
63
|
-
});
|
|
64
|
-
} catch (error) {
|
|
65
|
-
if (!isLockTimeoutError(error, "reconcile", "global")) throw error;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return await new Promise<string>((resolve, reject) => {
|
|
69
|
-
const child = spawn(process.execPath, [authWorkerPath, JSON.stringify({ config, configLoad: authConfigGuidance })], {
|
|
70
|
-
cwd,
|
|
71
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
let stdout = "";
|
|
75
|
-
let stderr = "";
|
|
76
|
-
child.stdout.on("data", (data) => {
|
|
77
|
-
stdout += String(data);
|
|
78
|
-
});
|
|
79
|
-
child.stderr.on("data", (data) => {
|
|
80
|
-
stderr += String(data);
|
|
81
|
-
});
|
|
82
|
-
child.on("error", (error) => reject(error));
|
|
83
|
-
child.on("close", (code) => {
|
|
84
|
-
const message = stdout.trim() || stderr.trim() || "Oracle auth bootstrap finished with no output.";
|
|
85
|
-
if (code === 0) resolve(message);
|
|
86
|
-
else reject(new Error(message));
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
66
|
export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string, workerPath: string): void {
|
|
92
67
|
pi.registerCommand("oracle-auth", {
|
|
93
68
|
description: "Sync ChatGPT cookies from real Chrome into the oracle auth seed profile",
|
|
94
69
|
handler: async (_args, ctx) => {
|
|
95
70
|
ctx.ui.notify("Syncing ChatGPT cookies from real Chrome into the oracle auth seed profile…", "info");
|
|
96
71
|
try {
|
|
97
|
-
const result = await
|
|
72
|
+
const result = await runOracleAuthBootstrap(authWorkerPath, ctx.cwd);
|
|
98
73
|
ctx.ui.notify(result, "info");
|
|
99
74
|
} catch (error) {
|
|
100
75
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
@@ -103,7 +78,7 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
103
78
|
});
|
|
104
79
|
|
|
105
80
|
pi.registerCommand("oracle-status", {
|
|
106
|
-
description: "Show oracle job status",
|
|
81
|
+
description: "Show oracle job status and recent job ids",
|
|
107
82
|
handler: async (args, ctx) => {
|
|
108
83
|
const explicitJobId = args.trim();
|
|
109
84
|
const jobId = explicitJobId || getLatestJobId(ctx.cwd);
|
|
@@ -123,12 +98,14 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
123
98
|
cwd: ctx.cwd,
|
|
124
99
|
});
|
|
125
100
|
}
|
|
126
|
-
|
|
101
|
+
const summary = await summarizeJob(job.id);
|
|
102
|
+
const recentJobs = !explicitJobId ? listRecentJobIds(ctx.cwd) : undefined;
|
|
103
|
+
ctx.ui.notify([summary, recentJobs ? `Recent jobs: ${recentJobs}` : undefined].filter(Boolean).join("\n"), "info");
|
|
127
104
|
},
|
|
128
105
|
});
|
|
129
106
|
|
|
130
|
-
pi.registerCommand("oracle-
|
|
131
|
-
description: "
|
|
107
|
+
pi.registerCommand("oracle-read", {
|
|
108
|
+
description: "Show oracle job status plus saved response preview",
|
|
132
109
|
handler: async (args, ctx) => {
|
|
133
110
|
const explicitJobId = args.trim();
|
|
134
111
|
const jobId = explicitJobId || getLatestJobId(ctx.cwd);
|
|
@@ -136,8 +113,32 @@ export function registerOracleCommands(pi: ExtensionAPI, authWorkerPath: string,
|
|
|
136
113
|
ctx.ui.notify("No oracle jobs found for this project", "info");
|
|
137
114
|
return;
|
|
138
115
|
}
|
|
116
|
+
const job = readScopedJob(jobId, ctx.cwd);
|
|
117
|
+
if (!job) {
|
|
118
|
+
ctx.ui.notify(`Oracle job ${jobId} was not found in this project`, "warning");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (isTerminalOracleJob(job)) {
|
|
122
|
+
await markWakeupSettled(job.id, {
|
|
123
|
+
source: "oracle_read_command",
|
|
124
|
+
sessionFile: ctx.sessionManager.getSessionFile?.(),
|
|
125
|
+
cwd: ctx.cwd,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
ctx.ui.notify(await summarizeJob(job.id, { responsePreview: true }), "info");
|
|
129
|
+
},
|
|
130
|
+
});
|
|
139
131
|
|
|
140
|
-
|
|
132
|
+
pi.registerCommand("oracle-cancel", {
|
|
133
|
+
description: "Cancel a queued or active oracle job by id",
|
|
134
|
+
handler: async (args, ctx) => {
|
|
135
|
+
const jobId = args.trim();
|
|
136
|
+
if (!jobId) {
|
|
137
|
+
ctx.ui.notify("Usage: /oracle-cancel <job-id>\nUse /oracle-status to find the job id you want to cancel.", "warning");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const job = readScopedJob(jobId, ctx.cwd);
|
|
141
142
|
if (!job) {
|
|
142
143
|
ctx.ui.notify(`Oracle job ${jobId} not found in this project`, "warning");
|
|
143
144
|
return;
|
|
@@ -8,6 +8,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
10
10
|
import { isAbsolute, join, normalize } from "node:path";
|
|
11
|
+
import { getProjectId } from "./runtime.js";
|
|
11
12
|
|
|
12
13
|
export const MODEL_FAMILIES = ["instant", "thinking", "pro"] as const;
|
|
13
14
|
export type OracleModelFamily = (typeof MODEL_FAMILIES)[number];
|
|
@@ -270,8 +271,9 @@ export interface OracleConfigLoadDetails {
|
|
|
270
271
|
|
|
271
272
|
export function getOracleConfigLoadDetails(cwd: string): OracleConfigLoadDetails {
|
|
272
273
|
const agentDir = getAgentDir();
|
|
274
|
+
const projectRoot = getProjectId(cwd);
|
|
273
275
|
const agentConfigPath = join(agentDir, "extensions", "oracle.json");
|
|
274
|
-
const projectConfigPath = join(
|
|
276
|
+
const projectConfigPath = join(projectRoot, ".pi", "extensions", "oracle.json");
|
|
275
277
|
return {
|
|
276
278
|
agentDir,
|
|
277
279
|
agentConfigPath,
|
|
@@ -34,7 +34,7 @@ import { cleanupRuntimeArtifacts, getProjectId, getSessionId, parseConversationI
|
|
|
34
34
|
export type OracleJobStatus = SharedOracleJobStatus;
|
|
35
35
|
export type OracleJobPhase = SharedOracleJobPhase;
|
|
36
36
|
|
|
37
|
-
export type OracleWakeupSettlementSource = "oracle_read" | "oracle_status";
|
|
37
|
+
export type OracleWakeupSettlementSource = "oracle_read" | "oracle_status" | "oracle_read_command";
|
|
38
38
|
|
|
39
39
|
export { ACTIVE_ORACLE_JOB_STATUSES, OPEN_ORACLE_JOB_STATUSES, TERMINAL_ORACLE_JOB_STATUSES };
|
|
40
40
|
export const ORACLE_MISSING_WORKER_GRACE_MS = 30_000;
|
|
@@ -728,8 +728,10 @@ export async function releaseNotificationClaim(jobId: string, claimedBy: string)
|
|
|
728
728
|
export async function noteWakeupRequested(jobId: string, at = new Date().toISOString()): Promise<OracleJob | undefined> {
|
|
729
729
|
try {
|
|
730
730
|
return await updateJob(jobId, (job) => noteOracleJobWakeupRequested(job, { at, source: "oracle:poller" }));
|
|
731
|
-
} catch {
|
|
732
|
-
|
|
731
|
+
} catch (error) {
|
|
732
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
733
|
+
if (message.startsWith("Oracle job not found:")) return undefined;
|
|
734
|
+
throw error;
|
|
733
735
|
}
|
|
734
736
|
}
|
|
735
737
|
|
|
@@ -927,7 +929,16 @@ export function resolveArchiveInputs(cwd: string, files: string[]): { absolute:
|
|
|
927
929
|
|
|
928
930
|
const realCwd = realpathSync(cwd);
|
|
929
931
|
return files.map((file) => {
|
|
932
|
+
if (!file.trim()) {
|
|
933
|
+
throw new Error("Archive input must be a non-empty project-relative path");
|
|
934
|
+
}
|
|
935
|
+
if (file.trim() === "." && file !== ".") {
|
|
936
|
+
throw new Error("Archive input must use '.' exactly for a whole-repo archive");
|
|
937
|
+
}
|
|
930
938
|
const absolute = resolve(cwd, file);
|
|
939
|
+
if (absolute === cwd && file !== ".") {
|
|
940
|
+
throw new Error("Archive input must use '.' exactly for a whole-repo archive");
|
|
941
|
+
}
|
|
931
942
|
const relative = absolute.startsWith(`${cwd}/`) ? absolute.slice(cwd.length + 1) : absolute === cwd ? "." : "";
|
|
932
943
|
if (!relative) {
|
|
933
944
|
throw new Error(`Archive input must be inside the project cwd: ${file}`);
|
|
@@ -245,8 +245,14 @@ async function scan(pi: ExtensionAPI, ctx: ExtensionContext, workerPath: string,
|
|
|
245
245
|
continue;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
|
|
249
|
-
|
|
248
|
+
const notedWakeup = await noteWakeupRequested(jobId);
|
|
249
|
+
const deliverableAfterNote = notedWakeup ?? readJob(jobId);
|
|
250
|
+
if (!deliverableAfterNote || shouldPruneTerminalJob(deliverableAfterNote, Date.now())) {
|
|
251
|
+
await releaseNotificationClaim(jobId, notificationClaimant).catch(() => undefined);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
requestWakeupTurn(pi, deliverableAfterNote);
|
|
250
256
|
if (ctx.hasUI) {
|
|
251
257
|
ctx.ui.notify(`Oracle job ${claimed.id} is ${claimed.status}.`, "info");
|
|
252
258
|
}
|