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 +10 -0
- package/README.md +4 -4
- package/docs/ORACLE_DESIGN.md +4 -4
- package/extensions/oracle/lib/poller.ts +8 -1
- package/extensions/oracle/lib/tools.ts +5 -2
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +0 -3
- package/extensions/oracle/shared/job-observability-helpers.d.mts +2 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +30 -2
- package/package.json +1 -1
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
|
|
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["
|
|
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 })`.
|
package/docs/ORACLE_DESIGN.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
490
|
-
- manual
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
"
|
|
230
|
+
"Do not poll now; wait for the one-time oracle completion wake-up.",
|
|
203
231
|
]
|
|
204
232
|
.filter(Boolean)
|
|
205
233
|
.join("\n");
|