pi-oracle 0.3.3 → 0.4.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 (37) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -0
  3. package/docs/ORACLE_DESIGN.md +1 -1
  4. package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +249 -0
  5. package/docs/ORACLE_RECOVERY_DRILL.md +5 -4
  6. package/extensions/oracle/index.ts +8 -1
  7. package/extensions/oracle/lib/commands.ts +11 -24
  8. package/extensions/oracle/lib/config.ts +5 -0
  9. package/extensions/oracle/lib/jobs.ts +117 -217
  10. package/extensions/oracle/lib/locks.ts +41 -209
  11. package/extensions/oracle/lib/poller.ts +14 -51
  12. package/extensions/oracle/lib/queue.ts +75 -112
  13. package/extensions/oracle/lib/runtime.ts +60 -14
  14. package/extensions/oracle/lib/tools.ts +70 -67
  15. package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
  16. package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
  17. package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +130 -0
  18. package/extensions/oracle/shared/job-lifecycle-helpers.mjs +377 -0
  19. package/extensions/oracle/shared/job-observability-helpers.d.mts +59 -0
  20. package/extensions/oracle/shared/job-observability-helpers.mjs +143 -0
  21. package/extensions/oracle/shared/process-helpers.d.mts +20 -0
  22. package/extensions/oracle/shared/process-helpers.mjs +128 -0
  23. package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
  24. package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
  25. package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
  26. package/extensions/oracle/worker/auth-bootstrap.mjs +100 -139
  27. package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
  28. package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
  29. package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
  30. package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
  31. package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
  32. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +33 -0
  33. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +292 -0
  34. package/extensions/oracle/worker/run-job.mjs +235 -380
  35. package/extensions/oracle/worker/state-locks.mjs +31 -216
  36. package/package.json +14 -5
  37. package/prompts/oracle.md +1 -1
@@ -0,0 +1,130 @@
1
+ export type OracleJobStatus = "queued" | "preparing" | "submitted" | "waiting" | "complete" | "failed" | "cancelled";
2
+
3
+ export type OracleJobPhase =
4
+ | "queued"
5
+ | "submitted"
6
+ | "cloning_runtime"
7
+ | "launching_browser"
8
+ | "verifying_auth"
9
+ | "configuring_model"
10
+ | "uploading_archive"
11
+ | "awaiting_response"
12
+ | "extracting_response"
13
+ | "downloading_artifacts"
14
+ | "complete"
15
+ | "complete_with_artifact_errors"
16
+ | "failed"
17
+ | "cancelled";
18
+
19
+ export interface OracleJobLifecycleEvent {
20
+ at: string;
21
+ source: string;
22
+ kind: "created" | "phase" | "cleanup" | "notification" | "wakeup";
23
+ status: OracleJobStatus;
24
+ phase: OracleJobPhase;
25
+ message: string;
26
+ }
27
+
28
+ export interface OracleLifecycleTrackedJobLike {
29
+ status: OracleJobStatus;
30
+ phase: OracleJobPhase;
31
+ phaseAt: string;
32
+ createdAt: string;
33
+ queuedAt?: string;
34
+ submittedAt?: string;
35
+ completedAt?: string;
36
+ heartbeatAt?: string;
37
+ lifecycleEvents?: OracleJobLifecycleEvent[];
38
+ cleanupPending?: boolean;
39
+ cleanupWarnings?: string[];
40
+ lastCleanupAt?: string;
41
+ notifyClaimedAt?: string;
42
+ notifyClaimedBy?: string;
43
+ notifiedAt?: string;
44
+ notificationEntryId?: string;
45
+ notificationSessionKey?: string;
46
+ notificationSessionFile?: string;
47
+ wakeupAttemptCount?: number;
48
+ wakeupLastRequestedAt?: string;
49
+ wakeupSettledAt?: string;
50
+ wakeupSettledSource?: string;
51
+ wakeupSettledSessionFile?: string;
52
+ wakeupSettledSessionKey?: string;
53
+ wakeupSettledBeforeFirstAttempt?: boolean;
54
+ wakeupObservedAt?: string;
55
+ wakeupObservedSource?: string;
56
+ wakeupObservedSessionFile?: string;
57
+ wakeupObservedSessionKey?: string;
58
+ error?: string;
59
+ artifactFailureCount?: number;
60
+ responsePath?: string;
61
+ responseFormat?: "text/plain";
62
+ }
63
+
64
+ export interface OraclePhaseTransitionOptions<TJob extends OracleLifecycleTrackedJobLike> {
65
+ at?: string;
66
+ source?: string;
67
+ message?: string;
68
+ patch?: Partial<TJob>;
69
+ clearNotificationClaim?: boolean;
70
+ }
71
+
72
+ export interface OracleWakeupSettlementOptions {
73
+ source: string;
74
+ at?: string;
75
+ sessionFile?: string;
76
+ sessionKey?: string;
77
+ allowBeforeFirstAttempt?: boolean;
78
+ }
79
+
80
+ export interface OracleNotificationTargetOptions {
81
+ at?: string;
82
+ source?: string;
83
+ notificationSessionKey: string;
84
+ notificationSessionFile?: string;
85
+ }
86
+
87
+ export interface OracleMarkNotifiedOptions {
88
+ at?: string;
89
+ source?: string;
90
+ notificationEntryId?: string;
91
+ notificationSessionKey?: string;
92
+ notificationSessionFile?: string;
93
+ }
94
+
95
+ export const ACTIVE_ORACLE_JOB_STATUSES: readonly OracleJobStatus[];
96
+ export const OPEN_ORACLE_JOB_STATUSES: readonly OracleJobStatus[];
97
+ export const TERMINAL_ORACLE_JOB_STATUSES: readonly OracleJobStatus[];
98
+ export const MAX_ORACLE_JOB_LIFECYCLE_EVENTS: number;
99
+
100
+ export declare function getOracleJobStatusForPhase(phase: OracleJobPhase): OracleJobStatus;
101
+ export declare function assertValidOracleJobState<TJob extends OracleLifecycleTrackedJobLike>(job: TJob): TJob;
102
+ export declare function appendOracleJobLifecycleEvent<TJob extends OracleLifecycleTrackedJobLike>(
103
+ job: TJob,
104
+ event: Omit<OracleJobLifecycleEvent, "status" | "phase"> & { status?: OracleJobStatus; phase?: OracleJobPhase },
105
+ ): TJob;
106
+ export declare function getLatestOracleJobLifecycleEvent(job: Pick<OracleLifecycleTrackedJobLike, "lifecycleEvents">): OracleJobLifecycleEvent | undefined;
107
+ export declare function markOracleJobCreated<TJob extends OracleLifecycleTrackedJobLike>(job: TJob, options?: { at?: string; source?: string; message?: string }): TJob;
108
+ export declare function transitionOracleJobPhase<TJob extends OracleLifecycleTrackedJobLike>(
109
+ job: TJob,
110
+ phase: OracleJobPhase,
111
+ options?: OraclePhaseTransitionOptions<TJob>,
112
+ ): TJob;
113
+ export declare function applyOracleJobCleanupWarnings<TJob extends OracleLifecycleTrackedJobLike>(
114
+ job: TJob,
115
+ warnings: string[],
116
+ options?: { at?: string; source?: string; message?: string },
117
+ ): TJob;
118
+ export declare function clearOracleJobCleanupState<TJob extends OracleLifecycleTrackedJobLike>(
119
+ job: TJob,
120
+ options?: { at?: string; source?: string; message?: string },
121
+ ): TJob;
122
+ export declare function claimOracleJobNotification<TJob extends OracleLifecycleTrackedJobLike>(job: TJob, claimedBy: string, at?: string): TJob;
123
+ export declare function recordOracleJobNotificationTarget<TJob extends OracleLifecycleTrackedJobLike>(job: TJob, options: OracleNotificationTargetOptions): TJob;
124
+ export declare function markOracleJobNotified<TJob extends OracleLifecycleTrackedJobLike>(job: TJob, options?: OracleMarkNotifiedOptions): TJob;
125
+ export declare function releaseOracleJobNotificationClaim<TJob extends OracleLifecycleTrackedJobLike>(job: TJob): TJob;
126
+ export declare function noteOracleJobWakeupRequested<TJob extends OracleLifecycleTrackedJobLike>(job: TJob, options?: { at?: string; source?: string }): TJob;
127
+ export declare function markOracleJobWakeupSettled<TJob extends OracleLifecycleTrackedJobLike>(
128
+ job: TJob,
129
+ options: OracleWakeupSettlementOptions,
130
+ ): TJob;
@@ -0,0 +1,377 @@
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
+ * @template {OracleLifecycleTrackedJobLike} TJob
116
+ * @param {TJob} job
117
+ * @param {{ at?: string; source?: string; message?: string }} [options]
118
+ * @returns {TJob}
119
+ */
120
+ export function markOracleJobCreated(job, options = {}) {
121
+ const at = options.at ?? job.createdAt;
122
+ return appendOracleJobLifecycleEvent(assertValidOracleJobState(job), {
123
+ at,
124
+ source: options.source ?? "oracle:create",
125
+ kind: "created",
126
+ message: options.message ?? `Job created in ${job.status} state.`,
127
+ });
128
+ }
129
+
130
+ /**
131
+ * @template {OracleLifecycleTrackedJobLike} TJob
132
+ * @param {TJob} job
133
+ * @param {OracleJobPhase} phase
134
+ * @param {{ at?: string; source?: string; message?: string; patch?: Partial<TJob>; clearNotificationClaim?: boolean }} [options]
135
+ * @returns {TJob}
136
+ */
137
+ export function transitionOracleJobPhase(job, phase, options = {}) {
138
+ const at = options.at ?? new Date().toISOString();
139
+ const patch = options.patch || {};
140
+ const status = patch.status ?? getOracleJobStatusForPhase(phase);
141
+ if (status !== getOracleJobStatusForPhase(phase)) {
142
+ throw new Error(`Invalid oracle job transition: phase ${phase} requires status ${getOracleJobStatusForPhase(phase)}, got ${String(status)}`);
143
+ }
144
+
145
+ /** @type {TJob} */
146
+ const next = {
147
+ ...job,
148
+ ...patch,
149
+ status,
150
+ phase,
151
+ phaseAt: at,
152
+ ...(phase === "queued" ? { queuedAt: patch.queuedAt ?? job.queuedAt ?? at } : {}),
153
+ ...(["submitted", "waiting"].includes(status) || patch.submittedAt !== undefined || job.submittedAt !== undefined
154
+ ? { submittedAt: patch.submittedAt ?? job.submittedAt ?? at }
155
+ : {}),
156
+ ...(TERMINAL_ORACLE_JOB_STATUSES.includes(status)
157
+ ? { completedAt: patch.completedAt ?? job.completedAt ?? at }
158
+ : { completedAt: patch.completedAt ?? job.completedAt }),
159
+ ...(options.clearNotificationClaim
160
+ ? { notifyClaimedAt: undefined, notifyClaimedBy: undefined }
161
+ : {}),
162
+ };
163
+
164
+ const validated = assertValidOracleJobState(next);
165
+ const changed = job.phase !== validated.phase || job.status !== validated.status || Boolean(options.message);
166
+ if (!changed) return validated;
167
+
168
+ return appendOracleJobLifecycleEvent(validated, {
169
+ at,
170
+ source: options.source ?? "oracle:lifecycle",
171
+ kind: "phase",
172
+ message: options.message ?? `Transitioned to ${validated.phase} (${validated.status}).`,
173
+ });
174
+ }
175
+
176
+ /**
177
+ * @template {OracleLifecycleTrackedJobLike} TJob
178
+ * @param {TJob} job
179
+ * @param {string[]} warnings
180
+ * @param {{ at?: string; source?: string; message?: string }} [options]
181
+ * @returns {TJob}
182
+ */
183
+ export function applyOracleJobCleanupWarnings(job, warnings, options = {}) {
184
+ if (warnings.length === 0) return assertValidOracleJobState(job);
185
+ const at = options.at ?? new Date().toISOString();
186
+ const next = assertValidOracleJobState({
187
+ ...job,
188
+ cleanupPending: false,
189
+ cleanupWarnings: Array.from(new Set([...(job.cleanupWarnings || []), ...warnings])),
190
+ lastCleanupAt: at,
191
+ error: [job.error, ...warnings].filter(Boolean).join("\n"),
192
+ });
193
+ return appendOracleJobLifecycleEvent(next, {
194
+ at,
195
+ source: options.source ?? "oracle:cleanup",
196
+ kind: "cleanup",
197
+ message: options.message ?? `Cleanup completed with ${warnings.length} warning(s).`,
198
+ });
199
+ }
200
+
201
+ /**
202
+ * @template {OracleLifecycleTrackedJobLike} TJob
203
+ * @param {TJob} job
204
+ * @param {{ at?: string; source?: string; message?: string }} [options]
205
+ * @returns {TJob}
206
+ */
207
+ export function clearOracleJobCleanupState(job, options = {}) {
208
+ const at = options.at ?? new Date().toISOString();
209
+ const next = assertValidOracleJobState({
210
+ ...job,
211
+ cleanupPending: false,
212
+ cleanupWarnings: undefined,
213
+ lastCleanupAt: at,
214
+ });
215
+ return appendOracleJobLifecycleEvent(next, {
216
+ at,
217
+ source: options.source ?? "oracle:cleanup",
218
+ kind: "cleanup",
219
+ message: options.message ?? "Cleanup finished without warnings.",
220
+ });
221
+ }
222
+
223
+ /**
224
+ * @template {OracleLifecycleTrackedJobLike} TJob
225
+ * @param {TJob} job
226
+ * @param {string} claimedBy
227
+ * @param {string} [at]
228
+ * @returns {TJob}
229
+ */
230
+ export function claimOracleJobNotification(job, claimedBy, at = new Date().toISOString()) {
231
+ return assertValidOracleJobState({
232
+ ...job,
233
+ notifyClaimedBy: claimedBy,
234
+ notifyClaimedAt: at,
235
+ });
236
+ }
237
+
238
+ /**
239
+ * @template {OracleLifecycleTrackedJobLike} TJob
240
+ * @param {TJob} job
241
+ * @param {{ at?: string; source?: string; notificationSessionKey: string; notificationSessionFile?: string }} options
242
+ * @returns {TJob}
243
+ */
244
+ export function recordOracleJobNotificationTarget(job, options) {
245
+ const at = options.at ?? new Date().toISOString();
246
+ const next = assertValidOracleJobState({
247
+ ...job,
248
+ notificationSessionKey: options.notificationSessionKey,
249
+ notificationSessionFile: options.notificationSessionFile,
250
+ });
251
+ return appendOracleJobLifecycleEvent(next, {
252
+ at,
253
+ source: options.source ?? "oracle:poller",
254
+ kind: "notification",
255
+ message: `Notification target recorded for ${options.notificationSessionKey}.`,
256
+ });
257
+ }
258
+
259
+ /**
260
+ * @template {OracleLifecycleTrackedJobLike} TJob
261
+ * @param {TJob} job
262
+ * @param {{ at?: string; source?: string; notificationEntryId?: string; notificationSessionKey?: string; notificationSessionFile?: string }} [options]
263
+ * @returns {TJob}
264
+ */
265
+ export function markOracleJobNotified(job, options = {}) {
266
+ const at = options.at ?? new Date().toISOString();
267
+ const next = assertValidOracleJobState({
268
+ ...job,
269
+ notifiedAt: at,
270
+ notificationEntryId: options.notificationEntryId ?? job.notificationEntryId,
271
+ notificationSessionKey: options.notificationSessionKey ?? job.notificationSessionKey,
272
+ notificationSessionFile: options.notificationSessionFile ?? job.notificationSessionFile,
273
+ wakeupAttemptCount: 0,
274
+ wakeupLastRequestedAt: undefined,
275
+ wakeupSettledAt: undefined,
276
+ notifyClaimedAt: undefined,
277
+ notifyClaimedBy: undefined,
278
+ });
279
+ return appendOracleJobLifecycleEvent(next, {
280
+ at,
281
+ source: options.source ?? "oracle:poller",
282
+ kind: "notification",
283
+ message: "Notification delivery recorded.",
284
+ });
285
+ }
286
+
287
+ /**
288
+ * @template {OracleLifecycleTrackedJobLike} TJob
289
+ * @param {TJob} job
290
+ * @returns {TJob}
291
+ */
292
+ export function releaseOracleJobNotificationClaim(job) {
293
+ return assertValidOracleJobState({
294
+ ...job,
295
+ notifyClaimedAt: undefined,
296
+ notifyClaimedBy: undefined,
297
+ });
298
+ }
299
+
300
+ /**
301
+ * @template {OracleLifecycleTrackedJobLike} TJob
302
+ * @param {TJob} job
303
+ * @param {{ at?: string; source?: string }} [options]
304
+ * @returns {TJob}
305
+ */
306
+ export function noteOracleJobWakeupRequested(job, options = {}) {
307
+ const at = options.at ?? new Date().toISOString();
308
+ const next = assertValidOracleJobState({
309
+ ...job,
310
+ wakeupAttemptCount: (job.wakeupAttemptCount ?? 0) + 1,
311
+ wakeupLastRequestedAt: at,
312
+ });
313
+ return appendOracleJobLifecycleEvent(next, {
314
+ at,
315
+ source: options.source ?? "oracle:poller",
316
+ kind: "wakeup",
317
+ message: `Wake-up reminder requested (attempt ${next.wakeupAttemptCount}).`,
318
+ });
319
+ }
320
+
321
+ /**
322
+ * @template {OracleLifecycleTrackedJobLike} TJob
323
+ * @param {TJob} job
324
+ * @param {{ source: string; at?: string; sessionFile?: string; sessionKey?: string; allowBeforeFirstAttempt?: boolean }} options
325
+ * @returns {TJob}
326
+ */
327
+ export function markOracleJobWakeupSettled(job, options) {
328
+ const at = options.at ?? new Date().toISOString();
329
+ const beforeFirstAttempt = !job.wakeupLastRequestedAt && (job.wakeupAttemptCount ?? 0) === 0;
330
+
331
+ if (job.wakeupSettledAt) {
332
+ const next = assertValidOracleJobState({
333
+ ...job,
334
+ wakeupSettledSource: job.wakeupSettledSource ?? options.source,
335
+ wakeupSettledSessionFile: job.wakeupSettledSessionFile ?? options.sessionFile,
336
+ wakeupSettledSessionKey: job.wakeupSettledSessionKey ?? options.sessionKey,
337
+ wakeupSettledBeforeFirstAttempt: job.wakeupSettledBeforeFirstAttempt ?? beforeFirstAttempt,
338
+ });
339
+ return appendOracleJobLifecycleEvent(next, {
340
+ at,
341
+ source: options.source,
342
+ kind: "wakeup",
343
+ message: `Wake-up already settled via ${next.wakeupSettledSource ?? options.source}.`,
344
+ });
345
+ }
346
+
347
+ if (beforeFirstAttempt && !options.allowBeforeFirstAttempt) {
348
+ const observed = assertValidOracleJobState({
349
+ ...job,
350
+ wakeupObservedAt: job.wakeupObservedAt ?? at,
351
+ wakeupObservedSource: job.wakeupObservedSource ?? options.source,
352
+ wakeupObservedSessionFile: job.wakeupObservedSessionFile ?? options.sessionFile,
353
+ wakeupObservedSessionKey: job.wakeupObservedSessionKey ?? options.sessionKey,
354
+ });
355
+ return appendOracleJobLifecycleEvent(observed, {
356
+ at,
357
+ source: options.source,
358
+ kind: "wakeup",
359
+ message: `Wake-up observed before the first reminder attempt via ${options.source}.`,
360
+ });
361
+ }
362
+
363
+ const settled = assertValidOracleJobState({
364
+ ...job,
365
+ wakeupSettledAt: at,
366
+ wakeupSettledSource: options.source,
367
+ wakeupSettledSessionFile: options.sessionFile,
368
+ wakeupSettledSessionKey: options.sessionKey,
369
+ wakeupSettledBeforeFirstAttempt: beforeFirstAttempt,
370
+ });
371
+ return appendOracleJobLifecycleEvent(settled, {
372
+ at,
373
+ source: options.source,
374
+ kind: "wakeup",
375
+ message: `Wake-up settled via ${options.source}.`,
376
+ });
377
+ }
@@ -0,0 +1,59 @@
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
+ includeLatestEvent?: boolean;
36
+ includeWorkerLogPath?: boolean;
37
+ }
38
+
39
+ export interface OracleSubmitResponseOptions {
40
+ autoPrunedPrefixes: Array<{ relativePath: string; bytes: number }>;
41
+ queued: boolean;
42
+ queuePosition?: number;
43
+ queueDepth?: number;
44
+ }
45
+
46
+ export interface OracleStatusCounts {
47
+ active: number;
48
+ queued: number;
49
+ }
50
+
51
+ export declare function formatBytes(bytes: number): string;
52
+ export declare function formatOracleLifecycleEvent(event: OracleJobLifecycleEvent | undefined): string | undefined;
53
+ export declare function formatOracleJobSummary(job: OracleJobSummaryLike, options?: OracleJobSummaryOptions): string;
54
+ export declare function buildOracleWakeupNotificationContent(
55
+ job: OracleJobSummaryLike,
56
+ options?: { responsePath?: string; artifactsPath?: string },
57
+ ): string;
58
+ export declare function formatOracleSubmitResponse(job: OracleJobSummaryLike & { promptPath: string; archivePath: string }, options: OracleSubmitResponseOptions): string;
59
+ export declare function buildOracleStatusText(counts: OracleStatusCounts): string;