pi-oracle 0.3.4 → 0.5.0

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +27 -8
  3. package/docs/ORACLE_DESIGN.md +14 -8
  4. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +276 -0
  5. package/extensions/oracle/index.ts +8 -1
  6. package/extensions/oracle/lib/commands.ts +25 -29
  7. package/extensions/oracle/lib/config.ts +56 -2
  8. package/extensions/oracle/lib/jobs.ts +134 -219
  9. package/extensions/oracle/lib/locks.ts +41 -209
  10. package/extensions/oracle/lib/poller.ts +38 -52
  11. package/extensions/oracle/lib/queue.ts +75 -112
  12. package/extensions/oracle/lib/runtime.ts +102 -19
  13. package/extensions/oracle/lib/tools.ts +663 -294
  14. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  15. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  16. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +131 -0
  17. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +390 -0
  18. package/extensions/oracle/shared/job-observability-helpers.d.mts +60 -0
  19. package/extensions/oracle/shared/job-observability-helpers.mjs +161 -0
  20. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  21. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  22. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  23. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  24. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  25. package/extensions/oracle/worker/auth-bootstrap.mjs +125 -134
  26. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  27. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  28. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  29. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  30. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  31. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
  32. package/extensions/oracle/worker/run-job.mjs +166 -274
  33. package/extensions/oracle/worker/state-locks.mjs +31 -216
  34. package/package.json +4 -3
  35. package/prompts/oracle.md +16 -10
@@ -0,0 +1,390 @@
1
+ // Purpose: Centralize oracle job lifecycle state transitions, invariants, and durable transition breadcrumbs.
2
+ // Responsibilities: Define valid phase/status relationships, apply lifecycle mutations, append bounded lifecycle events, and normalize cleanup/notification/wake-up state changes.
3
+ // Scope: Pure job-state reducers only; persistence, locking, browser work, and UI delivery stay in higher-level modules.
4
+ // Usage: Imported by extension lib code and worker code so all lifecycle transitions share the same invariants and event semantics.
5
+ // Invariants/Assumptions: Phase/status pairs must stay aligned, terminal jobs own completedAt, and cleanupPending is only legal for terminal states.
6
+
7
+ /** @typedef {import("./job-lifecycle-helpers.d.mts").OracleJobLifecycleEvent} OracleJobLifecycleEvent */
8
+ /** @typedef {import("./job-lifecycle-helpers.d.mts").OracleJobPhase} OracleJobPhase */
9
+ /** @typedef {import("./job-lifecycle-helpers.d.mts").OracleJobStatus} OracleJobStatus */
10
+ /** @typedef {import("./job-lifecycle-helpers.d.mts").OracleLifecycleTrackedJobLike} OracleLifecycleTrackedJobLike */
11
+
12
+ export const ACTIVE_ORACLE_JOB_STATUSES = Object.freeze(["preparing", "submitted", "waiting"]);
13
+ export const OPEN_ORACLE_JOB_STATUSES = Object.freeze(["queued", ...ACTIVE_ORACLE_JOB_STATUSES]);
14
+ export const TERMINAL_ORACLE_JOB_STATUSES = Object.freeze(["complete", "failed", "cancelled"]);
15
+ export const MAX_ORACLE_JOB_LIFECYCLE_EVENTS = 64;
16
+
17
+ /** @type {Record<OracleJobPhase, OracleJobStatus>} */
18
+ const PHASE_STATUS = Object.freeze({
19
+ queued: "queued",
20
+ submitted: "submitted",
21
+ cloning_runtime: "waiting",
22
+ launching_browser: "waiting",
23
+ verifying_auth: "waiting",
24
+ configuring_model: "waiting",
25
+ uploading_archive: "waiting",
26
+ awaiting_response: "waiting",
27
+ extracting_response: "waiting",
28
+ downloading_artifacts: "waiting",
29
+ complete: "complete",
30
+ complete_with_artifact_errors: "complete",
31
+ failed: "failed",
32
+ cancelled: "cancelled",
33
+ });
34
+
35
+ /**
36
+ * @param {OracleJobPhase} phase
37
+ * @returns {OracleJobStatus}
38
+ */
39
+ export function getOracleJobStatusForPhase(phase) {
40
+ return PHASE_STATUS[phase];
41
+ }
42
+
43
+ /**
44
+ * @template {OracleLifecycleTrackedJobLike} TJob
45
+ * @param {TJob} job
46
+ * @returns {TJob}
47
+ */
48
+ export function assertValidOracleJobState(job) {
49
+ const expectedStatus = PHASE_STATUS[job.phase];
50
+ if (!expectedStatus) {
51
+ throw new Error(`Invalid oracle job state: unknown phase ${String(job.phase)}`);
52
+ }
53
+ if (job.status !== expectedStatus) {
54
+ throw new Error(`Invalid oracle job state: phase ${job.phase} requires status ${expectedStatus}, got ${job.status}`);
55
+ }
56
+ if (job.status === "queued" && !job.queuedAt) {
57
+ throw new Error("Invalid oracle job state: queued jobs must record queuedAt");
58
+ }
59
+ if (["submitted", "waiting"].includes(job.status) && !job.submittedAt) {
60
+ throw new Error(`Invalid oracle job state: ${job.status} jobs must record submittedAt`);
61
+ }
62
+ if (TERMINAL_ORACLE_JOB_STATUSES.includes(job.status) && !job.completedAt) {
63
+ throw new Error(`Invalid oracle job state: terminal job ${job.status} must record completedAt`);
64
+ }
65
+ if (job.completedAt && !TERMINAL_ORACLE_JOB_STATUSES.includes(job.status)) {
66
+ throw new Error(`Invalid oracle job state: non-terminal job ${job.status} cannot record completedAt`);
67
+ }
68
+ if (job.cleanupPending && !TERMINAL_ORACLE_JOB_STATUSES.includes(job.status)) {
69
+ throw new Error(`Invalid oracle job state: non-terminal job ${job.status} cannot be cleanupPending`);
70
+ }
71
+ return job;
72
+ }
73
+
74
+ /**
75
+ * @param {Pick<OracleLifecycleTrackedJobLike, "phase" | "status" | "lifecycleEvents">} job
76
+ * @param {Omit<OracleJobLifecycleEvent, "status" | "phase"> & { status?: OracleJobStatus; phase?: OracleJobPhase }} event
77
+ * @returns {OracleJobLifecycleEvent[]}
78
+ */
79
+ function nextLifecycleEvents(job, event) {
80
+ const entry = {
81
+ at: event.at,
82
+ source: event.source,
83
+ kind: event.kind,
84
+ message: event.message,
85
+ status: event.status ?? job.status,
86
+ phase: event.phase ?? job.phase,
87
+ };
88
+ const events = [...(job.lifecycleEvents || []), entry];
89
+ return events.slice(-MAX_ORACLE_JOB_LIFECYCLE_EVENTS);
90
+ }
91
+
92
+ /**
93
+ * @template {OracleLifecycleTrackedJobLike} TJob
94
+ * @param {TJob} job
95
+ * @param {Omit<OracleJobLifecycleEvent, "status" | "phase"> & { status?: OracleJobStatus; phase?: OracleJobPhase }} event
96
+ * @returns {TJob}
97
+ */
98
+ export function appendOracleJobLifecycleEvent(job, event) {
99
+ return assertValidOracleJobState({
100
+ ...job,
101
+ lifecycleEvents: nextLifecycleEvents(job, event),
102
+ });
103
+ }
104
+
105
+ /**
106
+ * @param {Pick<OracleLifecycleTrackedJobLike, "lifecycleEvents">} job
107
+ * @returns {OracleJobLifecycleEvent | undefined}
108
+ */
109
+ export function getLatestOracleJobLifecycleEvent(job) {
110
+ const events = job.lifecycleEvents || [];
111
+ return events.length > 0 ? events[events.length - 1] : undefined;
112
+ }
113
+
114
+ /**
115
+ * @param {Pick<OracleLifecycleTrackedJobLike, "lifecycleEvents">} job
116
+ * @returns {OracleJobLifecycleEvent | undefined}
117
+ */
118
+ export function getLatestOracleTerminalLifecycleEvent(job) {
119
+ const events = job.lifecycleEvents || [];
120
+ for (let index = events.length - 1; index >= 0; index -= 1) {
121
+ const event = events[index];
122
+ if (event?.kind === "phase" && TERMINAL_ORACLE_JOB_STATUSES.includes(event.status)) return event;
123
+ }
124
+ return undefined;
125
+ }
126
+
127
+ /**
128
+ * @template {OracleLifecycleTrackedJobLike} TJob
129
+ * @param {TJob} job
130
+ * @param {{ at?: string; source?: string; message?: string }} [options]
131
+ * @returns {TJob}
132
+ */
133
+ export function markOracleJobCreated(job, options = {}) {
134
+ const at = options.at ?? job.createdAt;
135
+ return appendOracleJobLifecycleEvent(assertValidOracleJobState(job), {
136
+ at,
137
+ source: options.source ?? "oracle:create",
138
+ kind: "created",
139
+ message: options.message ?? `Job created in ${job.status} state.`,
140
+ });
141
+ }
142
+
143
+ /**
144
+ * @template {OracleLifecycleTrackedJobLike} TJob
145
+ * @param {TJob} job
146
+ * @param {OracleJobPhase} phase
147
+ * @param {{ at?: string; source?: string; message?: string; patch?: Partial<TJob>; clearNotificationClaim?: boolean }} [options]
148
+ * @returns {TJob}
149
+ */
150
+ export function transitionOracleJobPhase(job, phase, options = {}) {
151
+ const at = options.at ?? new Date().toISOString();
152
+ const patch = options.patch || {};
153
+ const status = patch.status ?? getOracleJobStatusForPhase(phase);
154
+ if (status !== getOracleJobStatusForPhase(phase)) {
155
+ throw new Error(`Invalid oracle job transition: phase ${phase} requires status ${getOracleJobStatusForPhase(phase)}, got ${String(status)}`);
156
+ }
157
+
158
+ /** @type {TJob} */
159
+ const next = {
160
+ ...job,
161
+ ...patch,
162
+ status,
163
+ phase,
164
+ phaseAt: at,
165
+ ...(phase === "queued" ? { queuedAt: patch.queuedAt ?? job.queuedAt ?? at } : {}),
166
+ ...(["submitted", "waiting"].includes(status) || patch.submittedAt !== undefined || job.submittedAt !== undefined
167
+ ? { submittedAt: patch.submittedAt ?? job.submittedAt ?? at }
168
+ : {}),
169
+ ...(TERMINAL_ORACLE_JOB_STATUSES.includes(status)
170
+ ? { completedAt: patch.completedAt ?? job.completedAt ?? at }
171
+ : { completedAt: patch.completedAt ?? job.completedAt }),
172
+ ...(options.clearNotificationClaim
173
+ ? { notifyClaimedAt: undefined, notifyClaimedBy: undefined }
174
+ : {}),
175
+ };
176
+
177
+ const validated = assertValidOracleJobState(next);
178
+ const changed = job.phase !== validated.phase || job.status !== validated.status || Boolean(options.message);
179
+ if (!changed) return validated;
180
+
181
+ return appendOracleJobLifecycleEvent(validated, {
182
+ at,
183
+ source: options.source ?? "oracle:lifecycle",
184
+ kind: "phase",
185
+ message: options.message ?? `Transitioned to ${validated.phase} (${validated.status}).`,
186
+ });
187
+ }
188
+
189
+ /**
190
+ * @template {OracleLifecycleTrackedJobLike} TJob
191
+ * @param {TJob} job
192
+ * @param {string[]} warnings
193
+ * @param {{ at?: string; source?: string; message?: string }} [options]
194
+ * @returns {TJob}
195
+ */
196
+ export function applyOracleJobCleanupWarnings(job, warnings, options = {}) {
197
+ if (warnings.length === 0) return assertValidOracleJobState(job);
198
+ const at = options.at ?? new Date().toISOString();
199
+ const next = assertValidOracleJobState({
200
+ ...job,
201
+ cleanupPending: false,
202
+ cleanupWarnings: Array.from(new Set([...(job.cleanupWarnings || []), ...warnings])),
203
+ lastCleanupAt: at,
204
+ error: [job.error, ...warnings].filter(Boolean).join("\n"),
205
+ });
206
+ return appendOracleJobLifecycleEvent(next, {
207
+ at,
208
+ source: options.source ?? "oracle:cleanup",
209
+ kind: "cleanup",
210
+ message: options.message ?? `Cleanup completed with ${warnings.length} warning(s).`,
211
+ });
212
+ }
213
+
214
+ /**
215
+ * @template {OracleLifecycleTrackedJobLike} TJob
216
+ * @param {TJob} job
217
+ * @param {{ at?: string; source?: string; message?: string }} [options]
218
+ * @returns {TJob}
219
+ */
220
+ export function clearOracleJobCleanupState(job, options = {}) {
221
+ const at = options.at ?? new Date().toISOString();
222
+ const next = assertValidOracleJobState({
223
+ ...job,
224
+ cleanupPending: false,
225
+ cleanupWarnings: undefined,
226
+ lastCleanupAt: at,
227
+ });
228
+ return appendOracleJobLifecycleEvent(next, {
229
+ at,
230
+ source: options.source ?? "oracle:cleanup",
231
+ kind: "cleanup",
232
+ message: options.message ?? "Cleanup finished without warnings.",
233
+ });
234
+ }
235
+
236
+ /**
237
+ * @template {OracleLifecycleTrackedJobLike} TJob
238
+ * @param {TJob} job
239
+ * @param {string} claimedBy
240
+ * @param {string} [at]
241
+ * @returns {TJob}
242
+ */
243
+ export function claimOracleJobNotification(job, claimedBy, at = new Date().toISOString()) {
244
+ return assertValidOracleJobState({
245
+ ...job,
246
+ notifyClaimedBy: claimedBy,
247
+ notifyClaimedAt: at,
248
+ });
249
+ }
250
+
251
+ /**
252
+ * @template {OracleLifecycleTrackedJobLike} TJob
253
+ * @param {TJob} job
254
+ * @param {{ at?: string; source?: string; notificationSessionKey: string; notificationSessionFile?: string }} options
255
+ * @returns {TJob}
256
+ */
257
+ export function recordOracleJobNotificationTarget(job, options) {
258
+ const at = options.at ?? new Date().toISOString();
259
+ const next = assertValidOracleJobState({
260
+ ...job,
261
+ notificationSessionKey: options.notificationSessionKey,
262
+ notificationSessionFile: options.notificationSessionFile,
263
+ });
264
+ return appendOracleJobLifecycleEvent(next, {
265
+ at,
266
+ source: options.source ?? "oracle:poller",
267
+ kind: "notification",
268
+ message: `Notification target recorded for ${options.notificationSessionKey}.`,
269
+ });
270
+ }
271
+
272
+ /**
273
+ * @template {OracleLifecycleTrackedJobLike} TJob
274
+ * @param {TJob} job
275
+ * @param {{ at?: string; source?: string; notificationEntryId?: string; notificationSessionKey?: string; notificationSessionFile?: string }} [options]
276
+ * @returns {TJob}
277
+ */
278
+ export function markOracleJobNotified(job, options = {}) {
279
+ const at = options.at ?? new Date().toISOString();
280
+ const next = assertValidOracleJobState({
281
+ ...job,
282
+ notifiedAt: at,
283
+ notificationEntryId: options.notificationEntryId ?? job.notificationEntryId,
284
+ notificationSessionKey: options.notificationSessionKey ?? job.notificationSessionKey,
285
+ notificationSessionFile: options.notificationSessionFile ?? job.notificationSessionFile,
286
+ wakeupAttemptCount: 0,
287
+ wakeupLastRequestedAt: undefined,
288
+ wakeupSettledAt: undefined,
289
+ notifyClaimedAt: undefined,
290
+ notifyClaimedBy: undefined,
291
+ });
292
+ return appendOracleJobLifecycleEvent(next, {
293
+ at,
294
+ source: options.source ?? "oracle:poller",
295
+ kind: "notification",
296
+ message: "Notification delivery recorded.",
297
+ });
298
+ }
299
+
300
+ /**
301
+ * @template {OracleLifecycleTrackedJobLike} TJob
302
+ * @param {TJob} job
303
+ * @returns {TJob}
304
+ */
305
+ export function releaseOracleJobNotificationClaim(job) {
306
+ return assertValidOracleJobState({
307
+ ...job,
308
+ notifyClaimedAt: undefined,
309
+ notifyClaimedBy: undefined,
310
+ });
311
+ }
312
+
313
+ /**
314
+ * @template {OracleLifecycleTrackedJobLike} TJob
315
+ * @param {TJob} job
316
+ * @param {{ at?: string; source?: string }} [options]
317
+ * @returns {TJob}
318
+ */
319
+ export function noteOracleJobWakeupRequested(job, options = {}) {
320
+ const at = options.at ?? new Date().toISOString();
321
+ const next = assertValidOracleJobState({
322
+ ...job,
323
+ wakeupAttemptCount: (job.wakeupAttemptCount ?? 0) + 1,
324
+ wakeupLastRequestedAt: at,
325
+ });
326
+ return appendOracleJobLifecycleEvent(next, {
327
+ at,
328
+ source: options.source ?? "oracle:poller",
329
+ kind: "wakeup",
330
+ message: `Wake-up reminder requested (attempt ${next.wakeupAttemptCount}).`,
331
+ });
332
+ }
333
+
334
+ /**
335
+ * @template {OracleLifecycleTrackedJobLike} TJob
336
+ * @param {TJob} job
337
+ * @param {{ source: string; at?: string; sessionFile?: string; sessionKey?: string; allowBeforeFirstAttempt?: boolean }} options
338
+ * @returns {TJob}
339
+ */
340
+ export function markOracleJobWakeupSettled(job, options) {
341
+ const at = options.at ?? new Date().toISOString();
342
+ const beforeFirstAttempt = !job.wakeupLastRequestedAt && (job.wakeupAttemptCount ?? 0) === 0;
343
+
344
+ if (job.wakeupSettledAt) {
345
+ const next = assertValidOracleJobState({
346
+ ...job,
347
+ wakeupSettledSource: job.wakeupSettledSource ?? options.source,
348
+ wakeupSettledSessionFile: job.wakeupSettledSessionFile ?? options.sessionFile,
349
+ wakeupSettledSessionKey: job.wakeupSettledSessionKey ?? options.sessionKey,
350
+ wakeupSettledBeforeFirstAttempt: job.wakeupSettledBeforeFirstAttempt ?? beforeFirstAttempt,
351
+ });
352
+ return appendOracleJobLifecycleEvent(next, {
353
+ at,
354
+ source: options.source,
355
+ kind: "wakeup",
356
+ message: `Wake-up already settled via ${next.wakeupSettledSource ?? options.source}.`,
357
+ });
358
+ }
359
+
360
+ if (beforeFirstAttempt && !options.allowBeforeFirstAttempt) {
361
+ const observed = assertValidOracleJobState({
362
+ ...job,
363
+ wakeupObservedAt: job.wakeupObservedAt ?? at,
364
+ wakeupObservedSource: job.wakeupObservedSource ?? options.source,
365
+ wakeupObservedSessionFile: job.wakeupObservedSessionFile ?? options.sessionFile,
366
+ wakeupObservedSessionKey: job.wakeupObservedSessionKey ?? options.sessionKey,
367
+ });
368
+ return appendOracleJobLifecycleEvent(observed, {
369
+ at,
370
+ source: options.source,
371
+ kind: "wakeup",
372
+ message: `Wake-up observed before the first reminder attempt via ${options.source}.`,
373
+ });
374
+ }
375
+
376
+ const settled = assertValidOracleJobState({
377
+ ...job,
378
+ wakeupSettledAt: at,
379
+ wakeupSettledSource: options.source,
380
+ wakeupSettledSessionFile: options.sessionFile,
381
+ wakeupSettledSessionKey: options.sessionKey,
382
+ wakeupSettledBeforeFirstAttempt: beforeFirstAttempt,
383
+ });
384
+ return appendOracleJobLifecycleEvent(settled, {
385
+ at,
386
+ source: options.source,
387
+ kind: "wakeup",
388
+ message: `Wake-up settled via ${options.source}.`,
389
+ });
390
+ }
@@ -0,0 +1,60 @@
1
+ import type { OracleJobLifecycleEvent } from "./job-lifecycle-helpers.d.mts";
2
+
3
+ export interface OracleJobSummaryLike {
4
+ id: string;
5
+ status: string;
6
+ phase: string;
7
+ createdAt: string;
8
+ queuedAt?: string;
9
+ submittedAt?: string;
10
+ completedAt?: string;
11
+ projectId: string;
12
+ sessionId: string;
13
+ followUpToJobId?: string;
14
+ chatUrl?: string;
15
+ conversationId?: string;
16
+ responsePath?: string;
17
+ responseFormat?: string;
18
+ artifactFailureCount?: number;
19
+ lastCleanupAt?: string;
20
+ cleanupWarnings?: string[];
21
+ error?: string;
22
+ workerLogPath?: string;
23
+ lifecycleEvents?: OracleJobLifecycleEvent[];
24
+ }
25
+
26
+ export interface OracleQueuePositionLike {
27
+ position: number;
28
+ depth: number;
29
+ }
30
+
31
+ export interface OracleJobSummaryOptions {
32
+ queuePosition?: OracleQueuePositionLike;
33
+ artifactsPath?: string;
34
+ responsePreview?: string;
35
+ responseAvailable?: boolean;
36
+ includeLatestEvent?: boolean;
37
+ includeWorkerLogPath?: boolean;
38
+ }
39
+
40
+ export interface OracleSubmitResponseOptions {
41
+ autoPrunedPrefixes: Array<{ relativePath: string; bytes: number }>;
42
+ queued: boolean;
43
+ queuePosition?: number;
44
+ queueDepth?: number;
45
+ }
46
+
47
+ export interface OracleStatusCounts {
48
+ active: number;
49
+ queued: number;
50
+ }
51
+
52
+ export declare function formatBytes(bytes: number): string;
53
+ export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEvent | undefined): string | undefined;
54
+ export declare function formatOracleJobSummary(job: OracleJobSummaryLike, options?: OracleJobSummaryOptions): string;
55
+ export declare function buildOracleWakeupNotificationContent(
56
+ job: OracleJobSummaryLike,
57
+ options?: { responsePath?: string; responseAvailable?: boolean; artifactsPath?: string },
58
+ ): string;
59
+ export declare function formatOracleSubmitResponse(job: OracleJobSummaryLike & { promptPath: string; archivePath: string }, options: OracleSubmitResponseOptions): string;
60
+ export declare function buildOracleStatusText(counts: OracleStatusCounts): string;
@@ -0,0 +1,161 @@
1
+ // Purpose: Provide shared oracle job observability formatting for UI messages, tool responses, and wake-up notifications.
2
+ // Responsibilities: Format job summaries, lifecycle breadcrumbs, submit responses, wake-up notification content, and session status text consistently across channels.
3
+ // Scope: Presentation helpers only; lifecycle mutation, persistence, and browser execution remain in lifecycle/job modules.
4
+ // Usage: Imported by commands, tools, poller, and extension startup/status code to keep detached-oracle messaging consistent.
5
+ // Invariants/Assumptions: Job summaries read from durable job state, and lifecycle event trails are bounded and already normalized by shared lifecycle helpers.
6
+
7
+ import { getLatestOracleJobLifecycleEvent, getLatestOracleTerminalLifecycleEvent } from "./job-lifecycle-helpers.mjs";
8
+
9
+ /** @typedef {import("./job-observability-helpers.d.mts").OracleJobSummaryLike} OracleJobSummaryLike */
10
+ /** @typedef {import("./job-observability-helpers.d.mts").OracleJobSummaryOptions} OracleJobSummaryOptions */
11
+ /** @typedef {import("./job-observability-helpers.d.mts").OracleStatusCounts} OracleStatusCounts */
12
+ /** @typedef {import("./job-observability-helpers.d.mts").OracleSubmitResponseOptions} OracleSubmitResponseOptions */
13
+ /** @typedef {import("./job-lifecycle-helpers.d.mts").OracleJobLifecycleEvent} OracleJobLifecycleEvent */
14
+
15
+ /**
16
+ * @param {number} bytes
17
+ * @returns {string}
18
+ */
19
+ export function formatBytes(bytes) {
20
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
21
+ const units = ["B", "KiB", "MiB", "GiB", "TiB"];
22
+ let value = bytes;
23
+ let unitIndex = 0;
24
+ while (value >= 1024 && unitIndex < units.length - 1) {
25
+ value /= 1024;
26
+ unitIndex += 1;
27
+ }
28
+ return `${value >= 10 || unitIndex === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`;
29
+ }
30
+
31
+ /**
32
+ * @param {OracleJobLifecycleEvent | undefined} event
33
+ * @returns {string | undefined}
34
+ */
35
+ export function formatOracleLifecycleEvent(event) {
36
+ if (!event) return undefined;
37
+ return `${event.at} [${event.source}] ${event.message}`;
38
+ }
39
+
40
+ /**
41
+ * @param {Array<{ relativePath: string; bytes: number }>} autoPrunedPrefixes
42
+ * @returns {string | undefined}
43
+ */
44
+ function formatAutoPrunedArchiveMessage(autoPrunedPrefixes) {
45
+ if (autoPrunedPrefixes.length === 0) return undefined;
46
+ return `Archive auto-pruned generic generated-output-name dirs to fit size limit: ${autoPrunedPrefixes.map((entry) => `${entry.relativePath}/ (${formatBytes(entry.bytes)})`).join(", ")}`;
47
+ }
48
+
49
+ /**
50
+ * @param {OracleJobSummaryLike} job
51
+ * @param {OracleJobSummaryOptions} [options]
52
+ * @returns {string}
53
+ */
54
+ export function formatOracleJobSummary(job, options = {}) {
55
+ const latestEventRaw = options.includeLatestEvent === false ? undefined : getLatestOracleJobLifecycleEvent(job);
56
+ const terminalEventRaw = getLatestOracleTerminalLifecycleEvent(job);
57
+ const latestEvent = formatOracleLifecycleEvent(latestEventRaw);
58
+ const terminalEvent = formatOracleLifecycleEvent(terminalEventRaw);
59
+ const sameEvent = Boolean(
60
+ latestEventRaw && terminalEventRaw &&
61
+ latestEventRaw.at === terminalEventRaw.at &&
62
+ latestEventRaw.source === terminalEventRaw.source &&
63
+ latestEventRaw.kind === terminalEventRaw.kind &&
64
+ latestEventRaw.message === terminalEventRaw.message,
65
+ );
66
+ const responseLine = options.responseAvailable === true
67
+ ? job.responsePath ? `response: ${job.responsePath}` : undefined
68
+ : job.responsePath ? "response: unavailable yet" : undefined;
69
+ const responseFormatLine = options.responseAvailable === true && job.responseFormat ? `response-format: ${job.responseFormat}` : undefined;
70
+ const latestEventLabel = latestEventRaw?.kind === "wakeup" ? "wakeup-event" : "last-event";
71
+ return [
72
+ `job: ${job.id}`,
73
+ `status: ${job.status}`,
74
+ `phase: ${job.phase}`,
75
+ `created: ${job.createdAt}`,
76
+ job.queuedAt ? `queued: ${job.queuedAt}` : undefined,
77
+ job.submittedAt ? `submitted: ${job.submittedAt}` : undefined,
78
+ options.queuePosition ? `queue-position: ${options.queuePosition.position} of ${options.queuePosition.depth} global` : undefined,
79
+ `project: ${job.projectId}`,
80
+ `session: ${job.sessionId}`,
81
+ job.completedAt ? `completed: ${job.completedAt}` : undefined,
82
+ job.followUpToJobId ? `follow-up-to: ${job.followUpToJobId}` : undefined,
83
+ job.chatUrl ? `chat: ${job.chatUrl}` : undefined,
84
+ job.conversationId ? `conversation: ${job.conversationId}` : undefined,
85
+ responseLine,
86
+ responseFormatLine,
87
+ options.artifactsPath ? `artifacts: ${options.artifactsPath}` : undefined,
88
+ typeof job.artifactFailureCount === "number" ? `artifact-failures: ${job.artifactFailureCount}` : undefined,
89
+ options.includeWorkerLogPath === false ? undefined : job.workerLogPath ? `worker-log: ${job.workerLogPath}` : undefined,
90
+ job.lastCleanupAt ? `last-cleanup: ${job.lastCleanupAt}` : undefined,
91
+ job.cleanupWarnings?.length ? `cleanup-warnings: ${job.cleanupWarnings.join(" | ")}` : undefined,
92
+ terminalEvent ? `terminal-event: ${terminalEvent}` : undefined,
93
+ latestEvent && !sameEvent ? `${latestEventLabel}: ${latestEvent}` : undefined,
94
+ job.error ? `error: ${job.error}` : undefined,
95
+ options.responsePreview ? "" : undefined,
96
+ options.responsePreview,
97
+ ]
98
+ .filter(Boolean)
99
+ .join("\n");
100
+ }
101
+
102
+ /**
103
+ * @param {OracleJobSummaryLike} job
104
+ * @param {{ responsePath?: string; responseAvailable?: boolean; artifactsPath?: string }} [options]
105
+ * @returns {string}
106
+ */
107
+ export function buildOracleWakeupNotificationContent(job, options = {}) {
108
+ const responseLine = options.responseAvailable === false
109
+ ? "Response file: unavailable yet"
110
+ : `Response file: ${options.responsePath ?? job.responsePath ?? `response unavailable for ${job.id}`}`;
111
+ const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
112
+ return [
113
+ `Oracle job ${job.id} is ${job.status}.`,
114
+ `Use oracle_read with jobId ${job.id} to open the response and settle wake-up retries.`,
115
+ responseLine,
116
+ `Artifacts: ${artifactsPath}`,
117
+ formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
118
+ job.error ? `Error: ${job.error}` : "After oracle_read, continue from the oracle output.",
119
+ ].filter(Boolean).join("\n");
120
+ }
121
+
122
+ /**
123
+ * @param {OracleJobSummaryLike & { promptPath: string; archivePath: string }} job
124
+ * @param {OracleSubmitResponseOptions} options
125
+ * @returns {string}
126
+ */
127
+ export function formatOracleSubmitResponse(job, options) {
128
+ return [
129
+ `${options.queued ? "Oracle job queued" : "Oracle job dispatched"}: ${job.id}`,
130
+ options.queued && options.queuePosition && options.queueDepth ? `Queue position: ${options.queuePosition} of ${options.queueDepth}` : undefined,
131
+ job.followUpToJobId ? `Follow-up to: ${job.followUpToJobId}` : undefined,
132
+ `Prompt: ${job.promptPath}`,
133
+ `Archive: ${job.archivePath}`,
134
+ formatAutoPrunedArchiveMessage(options.autoPrunedPrefixes),
135
+ `Response will be written to: ${job.responsePath}`,
136
+ formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
137
+ options.queued ? "The job will start automatically when capacity is available." : undefined,
138
+ "Stop now and wait for the oracle completion wake-up.",
139
+ ]
140
+ .filter(Boolean)
141
+ .join("\n");
142
+ }
143
+
144
+ /**
145
+ * @param {OracleStatusCounts} counts
146
+ * @returns {string}
147
+ */
148
+ export function buildOracleStatusText(counts) {
149
+ if (counts.active > 0 && counts.queued > 0) {
150
+ return `oracle: running (${counts.active}), queued (${counts.queued})`;
151
+ }
152
+ if (counts.active > 0) {
153
+ const suffix = counts.active > 1 ? ` (${counts.active})` : "";
154
+ return `oracle: running${suffix}`;
155
+ }
156
+ if (counts.queued > 0) {
157
+ const suffix = counts.queued > 1 ? ` (${counts.queued})` : "";
158
+ return `oracle: queued${suffix}`;
159
+ }
160
+ return "oracle: ready";
161
+ }
@@ -0,0 +1,20 @@
1
+ export interface OracleTrackedProcessOptions {
2
+ termGraceMs?: number;
3
+ killGraceMs?: number;
4
+ }
5
+
6
+ export interface OracleDetachedProcessHandle {
7
+ pid: number | undefined;
8
+ startedAt?: string;
9
+ }
10
+
11
+ export declare function readProcessStartedAt(pid: number | undefined): string | undefined;
12
+ export declare function isProcessAlive(pid: number | undefined): boolean;
13
+ export declare function isTrackedProcessAlive(pid: number | undefined, startedAt?: string): boolean;
14
+ export declare function waitForProcessStartedAt(pid: number | undefined, timeoutMs?: number): Promise<string | undefined>;
15
+ export declare function terminateTrackedProcess(
16
+ pid: number | undefined,
17
+ startedAt?: string,
18
+ options?: OracleTrackedProcessOptions,
19
+ ): Promise<boolean>;
20
+ export declare function spawnDetachedNodeProcess(scriptPath: string, args?: string[]): Promise<OracleDetachedProcessHandle>;