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 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 gather only minimal context and prefer a minimal archive instead of broad repo exploration. It should also omit `preset` and use the configured default model unless the task clearly needs a different one.
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. Only archive README.md unless another file is clearly necessary.
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 only the needed repo context"]
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-status [job-id]` — inspect job status
93
- - `/oracle-cancel [job-id]` — cancel queued or active job
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 `oracle_read(jobId)` or `/oracle-status`.
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-status [job-id]` or `oracle_read(jobId)`.
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
@@ -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 [job-id]`
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. gather only the smallest repo context needed to submit well
107
- 5. if the request is narrow, prefer a minimal targeted archive and dispatch as soon as enough context is in hand
108
- 6. if the request is broad/repo-wide, gather broader context and usually archive `.`
109
- 7. craft the oracle prompt
110
- 8. call `oracle_submit`
111
- 9. stop and wait for the completion wake-up (best-effort; durable oracle response/artifact state is already persisted outside session history)
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 `oracle_read(jobId)` as the canonical completion-consumption path, while still surfacing saved response/artifact paths as secondary context
477
- - manual `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
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 { formatOracleAuthConfigRemediation, formatOracleAuthConfigSummary, getOracleConfigLoadDetails, loadOracleConfig } from "./config.js";
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 runAuthBootstrap(authWorkerPath, ctx.cwd);
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
- ctx.ui.notify(summarizeJob(job.id), "info");
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-cancel", {
131
- description: "Cancel a queued or active oracle job",
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
- const job = explicitJobId ? readScopedJob(jobId, ctx.cwd) : readJob(jobId);
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(cwd, ".pi", "extensions", "oracle.json");
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
- return readJob(jobId);
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
- requestWakeupTurn(pi, deliverable);
249
- await noteWakeupRequested(jobId).catch(() => undefined);
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
  }