pi-oracle 0.3.4 → 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.
- package/CHANGELOG.md +21 -0
- package/README.md +2 -0
- package/docs/ORACLE_ISOLATED_PI_VALIDATION.md +249 -0
- package/extensions/oracle/index.ts +8 -1
- package/extensions/oracle/lib/commands.ts +11 -24
- package/extensions/oracle/lib/config.ts +5 -0
- package/extensions/oracle/lib/jobs.ts +117 -217
- package/extensions/oracle/lib/locks.ts +41 -209
- package/extensions/oracle/lib/poller.ts +14 -51
- package/extensions/oracle/lib/queue.ts +75 -112
- package/extensions/oracle/lib/runtime.ts +60 -14
- package/extensions/oracle/lib/tools.ts +66 -65
- package/extensions/oracle/shared/job-coordination-helpers.d.mts +84 -0
- package/extensions/oracle/shared/job-coordination-helpers.mjs +168 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.d.mts +130 -0
- package/extensions/oracle/shared/job-lifecycle-helpers.mjs +377 -0
- package/extensions/oracle/shared/job-observability-helpers.d.mts +59 -0
- package/extensions/oracle/shared/job-observability-helpers.mjs +143 -0
- package/extensions/oracle/shared/process-helpers.d.mts +20 -0
- package/extensions/oracle/shared/process-helpers.mjs +128 -0
- package/extensions/oracle/shared/state-coordination-helpers.d.mts +43 -0
- package/extensions/oracle/shared/state-coordination-helpers.mjs +381 -0
- package/extensions/oracle/worker/artifact-heuristics.mjs +5 -0
- package/extensions/oracle/worker/auth-bootstrap.mjs +76 -130
- package/extensions/oracle/worker/auth-cookie-policy.mjs +5 -0
- package/extensions/oracle/worker/auth-flow-helpers.d.mts +41 -0
- package/extensions/oracle/worker/auth-flow-helpers.mjs +165 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.d.mts +13 -0
- package/extensions/oracle/worker/chatgpt-flow-helpers.mjs +85 -0
- package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +93 -9
- package/extensions/oracle/worker/run-job.mjs +166 -274
- package/extensions/oracle/worker/state-locks.mjs +31 -216
- package/package.json +4 -3
|
@@ -0,0 +1,143 @@
|
|
|
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 } 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 latestEvent = options.includeLatestEvent === false ? undefined : formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job));
|
|
56
|
+
return [
|
|
57
|
+
`job: ${job.id}`,
|
|
58
|
+
`status: ${job.status}`,
|
|
59
|
+
`phase: ${job.phase}`,
|
|
60
|
+
`created: ${job.createdAt}`,
|
|
61
|
+
job.queuedAt ? `queued: ${job.queuedAt}` : undefined,
|
|
62
|
+
job.submittedAt ? `submitted: ${job.submittedAt}` : undefined,
|
|
63
|
+
options.queuePosition ? `queue-position: ${options.queuePosition.position} of ${options.queuePosition.depth} global` : undefined,
|
|
64
|
+
`project: ${job.projectId}`,
|
|
65
|
+
`session: ${job.sessionId}`,
|
|
66
|
+
job.completedAt ? `completed: ${job.completedAt}` : undefined,
|
|
67
|
+
job.followUpToJobId ? `follow-up-to: ${job.followUpToJobId}` : undefined,
|
|
68
|
+
job.chatUrl ? `chat: ${job.chatUrl}` : undefined,
|
|
69
|
+
job.conversationId ? `conversation: ${job.conversationId}` : undefined,
|
|
70
|
+
job.responsePath ? `response: ${job.responsePath}` : undefined,
|
|
71
|
+
job.responseFormat ? `response-format: ${job.responseFormat}` : undefined,
|
|
72
|
+
options.artifactsPath ? `artifacts: ${options.artifactsPath}` : undefined,
|
|
73
|
+
typeof job.artifactFailureCount === "number" ? `artifact-failures: ${job.artifactFailureCount}` : undefined,
|
|
74
|
+
options.includeWorkerLogPath === false ? undefined : job.workerLogPath ? `worker-log: ${job.workerLogPath}` : undefined,
|
|
75
|
+
job.lastCleanupAt ? `last-cleanup: ${job.lastCleanupAt}` : undefined,
|
|
76
|
+
job.cleanupWarnings?.length ? `cleanup-warnings: ${job.cleanupWarnings.join(" | ")}` : undefined,
|
|
77
|
+
latestEvent ? `last-event: ${latestEvent}` : undefined,
|
|
78
|
+
job.error ? `error: ${job.error}` : undefined,
|
|
79
|
+
options.responsePreview ? "" : undefined,
|
|
80
|
+
options.responsePreview,
|
|
81
|
+
]
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {OracleJobSummaryLike} job
|
|
88
|
+
* @param {{ responsePath?: string; artifactsPath?: string }} [options]
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
export function buildOracleWakeupNotificationContent(job, options = {}) {
|
|
92
|
+
const responsePath = options.responsePath ?? job.responsePath ?? `response unavailable for ${job.id}`;
|
|
93
|
+
const artifactsPath = options.artifactsPath ?? `artifacts unavailable for ${job.id}`;
|
|
94
|
+
return [
|
|
95
|
+
`Oracle job ${job.id} is ${job.status}.`,
|
|
96
|
+
`Use oracle_read with jobId ${job.id} to open the response and settle wake-up retries.`,
|
|
97
|
+
`Response file: ${responsePath}`,
|
|
98
|
+
`Artifacts: ${artifactsPath}`,
|
|
99
|
+
formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
|
|
100
|
+
job.error ? `Error: ${job.error}` : "After oracle_read, continue from the oracle output.",
|
|
101
|
+
].filter(Boolean).join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {OracleJobSummaryLike & { promptPath: string; archivePath: string }} job
|
|
106
|
+
* @param {OracleSubmitResponseOptions} options
|
|
107
|
+
* @returns {string}
|
|
108
|
+
*/
|
|
109
|
+
export function formatOracleSubmitResponse(job, options) {
|
|
110
|
+
return [
|
|
111
|
+
`${options.queued ? "Oracle job queued" : "Oracle job dispatched"}: ${job.id}`,
|
|
112
|
+
options.queued && options.queuePosition && options.queueDepth ? `Queue position: ${options.queuePosition} of ${options.queueDepth}` : undefined,
|
|
113
|
+
job.followUpToJobId ? `Follow-up to: ${job.followUpToJobId}` : undefined,
|
|
114
|
+
`Prompt: ${job.promptPath}`,
|
|
115
|
+
`Archive: ${job.archivePath}`,
|
|
116
|
+
formatAutoPrunedArchiveMessage(options.autoPrunedPrefixes),
|
|
117
|
+
`Response will be written to: ${job.responsePath}`,
|
|
118
|
+
formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job)) ? `Last event: ${formatOracleLifecycleEvent(getLatestOracleJobLifecycleEvent(job))}` : undefined,
|
|
119
|
+
options.queued ? "The job will start automatically when capacity is available." : undefined,
|
|
120
|
+
"Stop now and wait for the oracle completion wake-up.",
|
|
121
|
+
]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @param {OracleStatusCounts} counts
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
export function buildOracleStatusText(counts) {
|
|
131
|
+
if (counts.active > 0 && counts.queued > 0) {
|
|
132
|
+
return `oracle: running (${counts.active}), queued (${counts.queued})`;
|
|
133
|
+
}
|
|
134
|
+
if (counts.active > 0) {
|
|
135
|
+
const suffix = counts.active > 1 ? ` (${counts.active})` : "";
|
|
136
|
+
return `oracle: running${suffix}`;
|
|
137
|
+
}
|
|
138
|
+
if (counts.queued > 0) {
|
|
139
|
+
const suffix = counts.queued > 1 ? ` (${counts.queued})` : "";
|
|
140
|
+
return `oracle: queued${suffix}`;
|
|
141
|
+
}
|
|
142
|
+
return "oracle: ready";
|
|
143
|
+
}
|
|
@@ -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>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Purpose: Provide shared process-identity and termination helpers for oracle runtime, worker, and queue coordination.
|
|
2
|
+
// Responsibilities: Read stable process start identities, detect liveness, wait for freshly spawned processes, and terminate tracked processes safely.
|
|
3
|
+
// Scope: Local process coordination only; job-state mutation and queue semantics stay in higher-level helpers.
|
|
4
|
+
// Usage: Imported by lib/jobs.ts, lib/runtime.ts, worker/run-job.mjs, and shared state helpers.
|
|
5
|
+
// Invariants/Assumptions: Process identity is validated with `ps -o lstart=` to defend against PID reuse on macOS.
|
|
6
|
+
|
|
7
|
+
import { spawn, execFileSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("./process-helpers.d.mts").OracleTrackedProcessOptions} OracleTrackedProcessOptions */
|
|
10
|
+
/** @typedef {import("./process-helpers.d.mts").OracleDetachedProcessHandle} OracleDetachedProcessHandle */
|
|
11
|
+
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {number | undefined} pid
|
|
18
|
+
* @returns {string | undefined}
|
|
19
|
+
*/
|
|
20
|
+
export function readProcessStartedAt(pid) {
|
|
21
|
+
if (!pid || pid <= 0) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const startedAt = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" }).trim();
|
|
24
|
+
return startedAt || undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {number | undefined} pid
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function isProcessAlive(pid) {
|
|
35
|
+
if (!pid || pid <= 0) return false;
|
|
36
|
+
try {
|
|
37
|
+
process.kill(pid, 0);
|
|
38
|
+
return true;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ESRCH") return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {number | undefined} pid
|
|
47
|
+
* @param {string | undefined} startedAt
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function isTrackedProcessAlive(pid, startedAt) {
|
|
51
|
+
const currentStartedAt = readProcessStartedAt(pid);
|
|
52
|
+
if (!currentStartedAt) return false;
|
|
53
|
+
return startedAt ? currentStartedAt === startedAt : true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {number | undefined} pid
|
|
58
|
+
* @param {number} [timeoutMs]
|
|
59
|
+
* @returns {Promise<string | undefined>}
|
|
60
|
+
*/
|
|
61
|
+
export async function waitForProcessStartedAt(pid, timeoutMs = 2_000) {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
const startedAt = readProcessStartedAt(pid);
|
|
65
|
+
if (startedAt) return startedAt;
|
|
66
|
+
await sleep(100);
|
|
67
|
+
}
|
|
68
|
+
return readProcessStartedAt(pid);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {number | undefined} pid
|
|
73
|
+
* @param {string | undefined} startedAt
|
|
74
|
+
* @param {OracleTrackedProcessOptions} [options]
|
|
75
|
+
* @returns {Promise<boolean>}
|
|
76
|
+
*/
|
|
77
|
+
export async function terminateTrackedProcess(pid, startedAt, options = {}) {
|
|
78
|
+
if (!pid || pid <= 0) return true;
|
|
79
|
+
const currentStartedAt = readProcessStartedAt(pid);
|
|
80
|
+
if (!currentStartedAt) return true;
|
|
81
|
+
if (startedAt && currentStartedAt !== startedAt) return false;
|
|
82
|
+
|
|
83
|
+
const termGraceMs = options.termGraceMs ?? 5_000;
|
|
84
|
+
const killGraceMs = options.killGraceMs ?? 2_000;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, "SIGTERM");
|
|
88
|
+
} catch {
|
|
89
|
+
return !isTrackedProcessAlive(pid, startedAt);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const termDeadline = Date.now() + termGraceMs;
|
|
93
|
+
while (Date.now() < termDeadline) {
|
|
94
|
+
if (!isTrackedProcessAlive(pid, startedAt)) return true;
|
|
95
|
+
await sleep(250);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
process.kill(pid, "SIGKILL");
|
|
100
|
+
} catch {
|
|
101
|
+
return !isTrackedProcessAlive(pid, startedAt);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const killDeadline = Date.now() + killGraceMs;
|
|
105
|
+
while (Date.now() < killDeadline) {
|
|
106
|
+
if (!isTrackedProcessAlive(pid, startedAt)) return true;
|
|
107
|
+
await sleep(250);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return !isTrackedProcessAlive(pid, startedAt);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {string} scriptPath
|
|
115
|
+
* @param {string[]} args
|
|
116
|
+
* @returns {Promise<OracleDetachedProcessHandle>}
|
|
117
|
+
*/
|
|
118
|
+
export async function spawnDetachedNodeProcess(scriptPath, args = []) {
|
|
119
|
+
const child = spawn(process.execPath, [scriptPath, ...args], {
|
|
120
|
+
detached: true,
|
|
121
|
+
stdio: "ignore",
|
|
122
|
+
});
|
|
123
|
+
child.unref();
|
|
124
|
+
return {
|
|
125
|
+
pid: child.pid,
|
|
126
|
+
startedAt: await waitForProcessStartedAt(child.pid),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const ORACLE_METADATA_WRITE_GRACE_MS: number;
|
|
2
|
+
export const ORACLE_TMP_STATE_DIR_GRACE_MS: number;
|
|
3
|
+
|
|
4
|
+
export declare function hashOracleStateKey(kind: string, key: string): string;
|
|
5
|
+
export declare function getStateLocksDir(stateDir: string): string;
|
|
6
|
+
export declare function getStateLeasesDir(stateDir: string): string;
|
|
7
|
+
export declare function sweepStaleStateLocks(stateDir: string, now?: number): Promise<string[]>;
|
|
8
|
+
export declare function acquireStateLock(
|
|
9
|
+
stateDir: string,
|
|
10
|
+
kind: string,
|
|
11
|
+
key: string,
|
|
12
|
+
metadata: unknown,
|
|
13
|
+
timeoutMs?: number,
|
|
14
|
+
): Promise<string>;
|
|
15
|
+
export declare function releaseStatePath(path: string | undefined): Promise<void>;
|
|
16
|
+
export declare function withStateLock<T>(
|
|
17
|
+
stateDir: string,
|
|
18
|
+
kind: string,
|
|
19
|
+
key: string,
|
|
20
|
+
metadata: unknown,
|
|
21
|
+
fn: () => Promise<T>,
|
|
22
|
+
timeoutMs?: number,
|
|
23
|
+
): Promise<T>;
|
|
24
|
+
export declare function createStateLease(
|
|
25
|
+
stateDir: string,
|
|
26
|
+
kind: string,
|
|
27
|
+
key: string,
|
|
28
|
+
metadata: unknown,
|
|
29
|
+
timeoutMs?: number,
|
|
30
|
+
): Promise<string>;
|
|
31
|
+
export declare function writeStateLeaseMetadata(
|
|
32
|
+
stateDir: string,
|
|
33
|
+
kind: string,
|
|
34
|
+
key: string,
|
|
35
|
+
metadata: unknown,
|
|
36
|
+
): Promise<string>;
|
|
37
|
+
export declare function readStateLeaseMetadata<T = unknown>(
|
|
38
|
+
stateDir: string,
|
|
39
|
+
kind: string,
|
|
40
|
+
key: string,
|
|
41
|
+
): Promise<T | undefined>;
|
|
42
|
+
export declare function listStateLeaseMetadata<T = unknown>(stateDir: string, kind: string): T[];
|
|
43
|
+
export declare function releaseStateLease(stateDir: string, kind: string, key: string | undefined): Promise<void>;
|