pi-oracle 0.6.13 → 0.6.14

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
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.6.14 - 2026-05-02
6
+
7
+ ### Fixed
8
+ - deduped oracle completion wake-ups by marking one-time best-effort delivery in job state before the poller sends the follow-up turn, preventing repeated identical completion notifications across poller scans
9
+ - clarified completion wake-up guidance so agents treat the wake-up as a read/inspect prompt rather than an automatic auth refresh or resubmission instruction
10
+
11
+ ### Changed
12
+ - `oracle_read` and `/oracle-status` summaries for active jobs now include elapsed time, phase elapsed time, and a poll/backoff hint so manual checks waste fewer turns
13
+ - `oracle_submit` preset schema and prompt guidance now list the canonical preset ids directly for tool callers
14
+
5
15
  ## 0.6.13 - 2026-05-01
6
16
 
7
17
  ### Changed
package/README.md CHANGED
@@ -48,7 +48,7 @@ pi install https://github.com/fitchmultz/pi-oracle
48
48
  4. Optional: create `~/.pi/agent/extensions/oracle.json` if you want non-default settings.
49
49
  5. Run `/oracle-auth`.
50
50
  6. Run `/oracle Review the current pending changes. Include the whole repo unless a narrower archive is clearly better.`
51
- 7. Wait for a best-effort wake-up, or check `/oracle-status`.
51
+ 7. Wait for the one-time best-effort wake-up, or check `/oracle-status`.
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
 
@@ -56,7 +56,7 @@ For explicitly narrow requests, `/oracle` should still prefer a context-rich rel
56
56
 
57
57
  If a local archive still exceeds the 250 MB limit after default exclusions and automatic whole-repo pruning, the agent should treat that as a retryable archive-selection failure: shrink the archive automatically, retry with a smaller relevant slice, and explain what it cut only if it still cannot fit after the allowed retry budget.
58
58
 
59
- If you miss the wake-up, the result is still saved durably in the oracle job directory and can be read later.
59
+ If you miss the one-time wake-up, the result is still saved durably in the oracle job directory and can be read later.
60
60
 
61
61
  ## Example requests
62
62
 
@@ -87,7 +87,7 @@ flowchart LR
87
87
  C --> D["Detached worker starts isolated ChatGPT runtime"]
88
88
  D --> E["Archive + prompt sent to ChatGPT.com"]
89
89
  E --> F["Response/artifacts saved under oracle job dir"]
90
- F --> G["Best-effort wake-up to matching pi session"]
90
+ F --> G["One-time best-effort wake-up to matching pi session"]
91
91
  ```
92
92
 
93
93
  If concurrency is full, the job is queued and starts automatically later.
@@ -162,7 +162,7 @@ Project config should only override safe, non-privileged settings.
162
162
 
163
163
  - Jobs persist their response and any artifacts under `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-<job-id>/` by default.
164
164
  - Jobs can queue automatically if runtime capacity is full.
165
- - Completion delivery into `pi` is best-effort wake-up based.
165
+ - Completion delivery into `pi` is one-time best-effort wake-up based; duplicate poller scans are deduped in job state.
166
166
  - If you miss the wake-up, use `/oracle-read [job-id]` to inspect the saved response preview.
167
167
  - `/oracle-status [job-id]` still shows saved job metadata and lists recent job ids when you omit the id.
168
168
  - Agent callers can use `oracle_read({ jobId })`.
@@ -484,12 +484,12 @@ The extension still uses the same general `pi`-native background completion patt
484
484
  - detached worker writes `${PI_ORACLE_JOBS_DIR:-/tmp}/oracle-*` state
485
485
  - poller scans jobs on an interval
486
486
  - completed job durability lives in oracle job state plus saved response/artifact files, not in synthetic session-history assistant messages
487
- - 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
487
+ - when a matching job reaches `complete`, `failed`, or `cancelled`, the poller issues one best-effort wake-up to whichever matching session is currently live, then records `notifiedAt` so later scans do not duplicate the completion message
488
488
  - 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
489
- - 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
490
- - manual inspection before the first wake-up attempt is recorded separately as observation metadata and does not suppress the first reminder send
489
+ - wake-up content explicitly tells agents not to treat completion as an automatic `oracle_auth`, `oracle_submit`, or `oracle_cancel` retry instruction
490
+ - manual `oracle_read`, `/oracle-read`, or `/oracle-status` inspection after a wake-up persists provenance about which path/session settled the wake-up
491
491
  - 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
492
- - because completion delivery is best-effort, pruning uses explicit terminal-job age policy instead of pretending a durable session notification happened
492
+ - because completion delivery is best-effort, pruning uses explicit terminal-job age policy plus `notifiedAt`/wakeup state instead of pretending a durable session notification was appended
493
493
  - 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
494
494
 
495
495
  ## What was removed by this pivot
@@ -15,6 +15,7 @@ import {
15
15
  hasPersistedOriginSession,
16
16
  isActiveOracleJob,
17
17
  listOracleJobDirs,
18
+ markJobNotified,
18
19
  noteWakeupRequested,
19
20
  readJob,
20
21
  recordNotificationTarget,
@@ -324,7 +325,6 @@ async function scan(
324
325
  await releaseNotificationClaim(jobId, notificationClaimant).catch(() => undefined);
325
326
  return;
326
327
  }
327
- requestWakeupTurn(pi, deliverable);
328
328
  const notedWakeup = await noteWakeupRequested(jobId);
329
329
  if (!notedWakeup) {
330
330
  await releaseNotificationClaim(jobId, notificationClaimant).catch(() => undefined);
@@ -334,6 +334,13 @@ async function scan(
334
334
  await releaseNotificationClaim(jobId, notificationClaimant).catch(() => undefined);
335
335
  return;
336
336
  }
337
+ await hooks.beforeMarkJobNotified?.(deliverable);
338
+ await markJobNotified(jobId, notificationClaimant, {
339
+ notificationSessionKey: pollerKey,
340
+ notificationSessionFile: currentSessionFile,
341
+ });
342
+ if (await releaseWakeupLeaseIfInactive(wakeupTargetLeaseKey, lifecycle)) return;
343
+ requestWakeupTurn(pi, deliverable);
337
344
  if (snapshot.hasUI) {
338
345
  snapshot.ui.notify(`Oracle job ${claimed.id} is ${claimed.status}.`, "info");
339
346
  }
@@ -71,7 +71,8 @@ const ORACLE_SUBMIT_PARAMS = Type.Object({
71
71
  preset: Type.Optional(
72
72
  Type.String({
73
73
  description:
74
- "ChatGPT model preset. Omit to use the configured default preset. Canonical ids are preferred; matching human-readable preset labels and common hyphen/space variants are normalized automatically.",
74
+ `ChatGPT model preset. Omit to use the configured default preset. Canonical ids: ${ORACLE_SUBMIT_PRESET_IDS.join(", ")}. ` +
75
+ "Matching human-readable preset labels and common hyphen/space variants are normalized automatically.",
75
76
  }),
76
77
  ),
77
78
  followUpJobId: Type.Optional(Type.String({ description: "Earlier oracle job id whose chat thread should be continued." })),
@@ -1101,7 +1102,9 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1101
1102
  "For any other submit-time error, stop and report the error instead of retrying automatically.",
1102
1103
  "If oracle_submit returns a queued job instead of an immediately dispatched one, treat that as success and stop exactly the same way.",
1103
1104
  "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.",
1104
- "Use `preset` as the only model-selection parameter on oracle_submit. Canonical ids are preferred, and matching human-readable preset labels are normalized automatically. Omit preset to use the configured default.",
1105
+ "Use `preset` as the only model-selection parameter on oracle_submit. " +
1106
+ `Canonical ids: ${ORACLE_SUBMIT_PRESET_IDS.join(", ")}. ` +
1107
+ "matching human-readable preset labels are normalized automatically. Omit preset to use the configured default.",
1105
1108
  ],
1106
1109
  parameters: ORACLE_SUBMIT_PARAMS,
1107
1110
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
@@ -287,9 +287,6 @@ export function markOracleJobNotified(job, options = {}) {
287
287
  notificationEntryId: options.notificationEntryId ?? job.notificationEntryId,
288
288
  notificationSessionKey: options.notificationSessionKey ?? job.notificationSessionKey,
289
289
  notificationSessionFile: options.notificationSessionFile ?? job.notificationSessionFile,
290
- wakeupAttemptCount: 0,
291
- wakeupLastRequestedAt: undefined,
292
- wakeupSettledAt: undefined,
293
290
  notifyClaimedAt: undefined,
294
291
  notifyClaimedBy: undefined,
295
292
  });
@@ -4,6 +4,7 @@ export interface OracleJobSummaryLike {
4
4
  id: string;
5
5
  status: string;
6
6
  phase: string;
7
+ phaseAt?: string;
7
8
  createdAt: string;
8
9
  queuedAt?: string;
9
10
  submittedAt?: string;
@@ -38,6 +39,7 @@ export interface OracleJobSummaryOptions {
38
39
  includeWorkerLogPath?: boolean;
39
40
  nowMs?: number;
40
41
  heartbeatStaleMs?: number;
42
+ suggestedPollAfterSeconds?: number;
41
43
  }
42
44
 
43
45
  export interface OracleSubmitResponseOptions {
@@ -48,6 +48,7 @@ function formatAutoPrunedArchiveMessage(autoPrunedPrefixes) {
48
48
 
49
49
  const ACTIVE_SUMMARY_STATUSES = new Set(["preparing", "submitted", "waiting"]);
50
50
  const DEFAULT_ORACLE_HEARTBEAT_STALE_MS = 3 * 60 * 1000;
51
+ const DEFAULT_ACTIVE_JOB_POLL_HINT_SECONDS = 15;
51
52
 
52
53
  /**
53
54
  * @param {string | undefined} value
@@ -99,6 +100,31 @@ function formatHeartbeatFreshness(job, options = {}) {
99
100
  return `heartbeat: ${freshness} (${formatElapsed(elapsedMs)} ${submittedMs !== undefined ? "since submit" : "since create"})`;
100
101
  }
101
102
 
103
+ /**
104
+ * @param {OracleJobSummaryLike} job
105
+ * @param {OracleJobSummaryOptions} [options]
106
+ * @returns {string[]}
107
+ */
108
+ function formatActiveProgressLines(job, options = {}) {
109
+ if (!ACTIVE_SUMMARY_STATUSES.has(job.status)) return [];
110
+ const nowMs = Number.isFinite(options.nowMs) ? options.nowMs : Date.now();
111
+ const submittedMs = parseTimestamp(job.submittedAt);
112
+ const createdMs = parseTimestamp(job.createdAt);
113
+ const phaseMs = parseTimestamp(job.phaseAt);
114
+ const baselineMs = submittedMs ?? createdMs;
115
+ const elapsedLine = baselineMs === undefined
116
+ ? undefined
117
+ : `elapsed: ${formatElapsed(nowMs - baselineMs)} ${submittedMs !== undefined ? "since submit" : "since create"}`;
118
+ const phaseElapsedLine = phaseMs === undefined
119
+ ? undefined
120
+ : `phase-elapsed: ${formatElapsed(nowMs - phaseMs)} in ${job.phase}`;
121
+ const pollHintSeconds = Number.isFinite(options.suggestedPollAfterSeconds)
122
+ ? Math.max(1, Math.round(options.suggestedPollAfterSeconds))
123
+ : DEFAULT_ACTIVE_JOB_POLL_HINT_SECONDS;
124
+ const pollHint = `poll-hint: wait about ${pollHintSeconds}s before checking again, or stop and wait for the one-time completion wake-up`;
125
+ return [elapsedLine, phaseElapsedLine, pollHint].filter(Boolean);
126
+ }
127
+
102
128
  /**
103
129
  * @param {{ id: string; status: string }} job
104
130
  * @returns {string}
@@ -142,6 +168,7 @@ export function formatOracleJobSummary(job, options = {}) {
142
168
  `project: ${job.projectId}`,
143
169
  `session: ${job.sessionId}`,
144
170
  formatHeartbeatFreshness(job, options),
171
+ ...formatActiveProgressLines(job, options),
145
172
  job.completedAt ? `completed: ${job.completedAt}` : undefined,
146
173
  job.followUpToJobId ? `follow-up-to: ${job.followUpToJobId}` : undefined,
147
174
  job.chatUrl ? `chat: ${job.chatUrl}` : undefined,
@@ -175,7 +202,8 @@ export function buildOracleWakeupNotificationContent(job, options = {}) {
175
202
  const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
176
203
  return [
177
204
  `Oracle job ${job.id} is ${job.status}.`,
178
- `Use /oracle-read ${job.id} to inspect the saved response preview. /oracle-status ${job.id} still shows saved job metadata. Agent callers can use oracle_read({ jobId: "${job.id}" }) if they need tool output in the current turn.`,
205
+ "This is a one-time completion wake-up, not a retry instruction. Do not call oracle_auth, oracle_submit, or oracle_cancel automatically from this wake-up.",
206
+ `Use /oracle-read ${job.id} to inspect the saved response preview. /oracle-status ${job.id} still shows saved job metadata. Agent callers can use oracle_read({ jobId: "${job.id}" }) once if they need tool output in the current turn.`,
179
207
  responseLine,
180
208
  `Artifacts: ${artifactsPath}`,
181
209
  formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
@@ -199,7 +227,7 @@ export function formatOracleSubmitResponse(job, options) {
199
227
  `Response will be written to: ${job.responsePath}`,
200
228
  formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
201
229
  options.queued ? "The job will start automatically when capacity is available." : undefined,
202
- "Stop now and wait for the oracle completion wake-up.",
230
+ "Do not poll now; wait for the one-time oracle completion wake-up.",
203
231
  ]
204
232
  .filter(Boolean)
205
233
  .join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "ChatGPT web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",