pi-oracle 0.7.10 → 0.7.12

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.
@@ -4,13 +4,10 @@
4
4
  // Usage: Imported by the oracle extension entrypoint and sanity tests to register tools against the pi API.
5
5
  // Invariants/Assumptions: The pi runtime validates TypeBox schemas before execute, while execute owns semantic normalization.
6
6
  import { randomUUID } from "node:crypto";
7
- import { spawn } from "node:child_process";
8
- import { once } from "node:events";
9
- import { createReadStream } from "node:fs";
10
- import { lstat, mkdtemp, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { rename, rm, stat } from "node:fs/promises";
11
8
  import { tmpdir } from "node:os";
12
- import { basename, dirname, join, posix } from "node:path";
13
- import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
9
+ import { join } from "node:path";
10
+ import { createArchive, type ArchiveCreationResult, type ArchiveSizeBreakdownRow } from "./archive.js";
14
11
  import { runOracleAuthBootstrap } from "./auth.js";
15
12
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
16
13
  import { Type } from "typebox";
@@ -45,7 +42,6 @@ import {
45
42
  pruneTerminalOracleJobs,
46
43
  reconcileStaleOracleJobs,
47
44
  resolveArchiveInputs,
48
- sha256File,
49
45
  shouldAdvanceQueueAfterCancellation,
50
46
  spawnWorker,
51
47
  terminateWorkerPid,
@@ -53,6 +49,7 @@ import {
53
49
  type OracleJob,
54
50
  } from "./jobs.js";
55
51
  import { getQueuePosition, promoteQueuedJobs, promoteQueuedJobsWithinAdmissionLock } from "./queue.js";
52
+ import { resolveOracleProviderArchivePlan } from "./provider-capabilities.js";
56
53
  import { refreshOracleStatus } from "./poller.js";
57
54
  import {
58
55
  allocateRuntime,
@@ -124,568 +121,8 @@ const ORACLE_CANCEL_PARAMS = Type.Object({
124
121
  jobId: Type.String({ description: "Oracle job id." }),
125
122
  });
126
123
 
127
- const CHATGPT_MAX_ARCHIVE_BYTES = 250 * 1024 * 1024;
128
- const GROK_MAX_ARCHIVE_BYTES = 200 * 1024 * 1024;
129
- const MAX_ARCHIVE_BYTES = CHATGPT_MAX_ARCHIVE_BYTES;
130
124
  const MAX_QUEUED_JOBS_PER_ACTIVE_RUNTIME = 1;
131
- const MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME = MAX_ARCHIVE_BYTES;
132
- const ARCHIVE_COMMAND_TIMEOUT_MS = 120_000;
133
- const ARCHIVE_COMMAND_KILL_GRACE_MS = 2_000;
134
- const ARCHIVE_PIPE_FAILURE_ERROR_CODES = new Set(["EPIPE", "ERR_STREAM_DESTROYED"]);
135
-
136
- const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE = new Set([
137
- ".git",
138
- ".hg",
139
- ".svn",
140
- ".pi",
141
- ".oracle-context",
142
- ".cursor",
143
- ".artifacts",
144
- ".crabbox",
145
- "node_modules",
146
- "target",
147
- ".venv",
148
- "venv",
149
- "__pycache__",
150
- ".pytest_cache",
151
- ".mypy_cache",
152
- ".ruff_cache",
153
- ".tox",
154
- ".nox",
155
- ".hypothesis",
156
- ".next",
157
- ".nuxt",
158
- ".svelte-kit",
159
- ".turbo",
160
- ".parcel-cache",
161
- ".cache",
162
- ".gradle",
163
- ".terraform",
164
- "DerivedData",
165
- ".build",
166
- ".pnpm-store",
167
- ".serverless",
168
- ".aws-sam",
169
- "secrets",
170
- ".secrets",
171
- ]);
172
- const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_AT_REPO_ROOT = new Set(["coverage", "htmlcov", "tmp", "temp", ".tmp", "dist", "build", "out"]);
173
- const DEFAULT_ARCHIVE_EXCLUDED_FILES = new Set([
174
- ".coverage",
175
- ".DS_Store",
176
- ".env",
177
- ".netrc",
178
- ".npmrc",
179
- ".pypirc",
180
- ".scratchpad.md",
181
- "Thumbs.db",
182
- "id_dsa",
183
- "id_ecdsa",
184
- "id_ed25519",
185
- "id_rsa",
186
- ]);
187
- const DEFAULT_ARCHIVE_EXCLUDED_SUFFIXES = [".db", ".key", ".p12", ".pfx", ".pyc", ".pyd", ".pyo", ".pem", ".sqlite", ".sqlite3", ".tsbuildinfo", ".tfstate"];
188
- const DEFAULT_ARCHIVE_EXCLUDED_SUBSTRINGS = [".tfstate."];
189
- const DEFAULT_ARCHIVE_EXCLUDED_ENV_ALLOWLIST = new Set([".env.dist", ".env.example", ".env.sample", ".env.template"]);
190
- const DEFAULT_ARCHIVE_EXCLUDED_PATH_SEQUENCES = [[".yarn", "cache"]] as const;
191
- const ADAPTIVE_ARCHIVE_PRUNE_DIR_NAMES_ANYWHERE = new Set(["build", "dist", "out", "coverage", "htmlcov", "tmp", "temp", ".tmp"]);
192
- const ADAPTIVE_ARCHIVE_PRUNE_PROTECTED_ANCESTOR_DIR_NAMES = new Set(["src", "source", "sources", "lib"]);
193
-
194
- type ArchiveSizeBreakdownRow = { relativePath: string; bytes: number };
195
- type ArchiveCreationResult = {
196
- sha256: string;
197
- archiveBytes: number;
198
- initialArchiveBytes?: number;
199
- autoPrunedPrefixes: ArchiveSizeBreakdownRow[];
200
- includedEntries: string[];
201
- };
202
-
203
- function appendArchiveEntries(target: string[], source: Iterable<string>): void {
204
- for (const entry of source) target.push(entry);
205
- }
206
-
207
- function getErrorCode(error: unknown): string | undefined {
208
- return error && typeof error === "object" && "code" in error && typeof error.code === "string"
209
- ? error.code
210
- : undefined;
211
- }
212
-
213
- function mergeArchiveEntryGroups(groups: Iterable<Iterable<string>>): string[] {
214
- const merged: string[] = [];
215
- for (const group of groups) appendArchiveEntries(merged, group);
216
- return merged;
217
- }
218
-
219
- export function mergeArchiveEntryGroupsForTesting(groups: Iterable<Iterable<string>>): string[] {
220
- return mergeArchiveEntryGroups(groups);
221
- }
222
-
223
- function pathContainsSequence(relativePath: string, sequence: readonly string[]): boolean {
224
- const segments = relativePath.split("/").filter(Boolean);
225
- if (sequence.length === 0 || segments.length < sequence.length) return false;
226
- for (let index = 0; index <= segments.length - sequence.length; index += 1) {
227
- if (sequence.every((segment, offset) => segments[index + offset] === segment)) return true;
228
- }
229
- return false;
230
- }
231
-
232
- function getRelativeDepth(relativePath: string): number {
233
- return relativePath.split("/").filter(Boolean).length;
234
- }
235
-
236
- function formatBytes(bytes: number): string {
237
- return `${(bytes / (1024 * 1024)).toFixed(2)} MiB`;
238
- }
239
-
240
- function formatDirectoryLabel(relativePath: string): string {
241
- return relativePath.endsWith("/") ? relativePath : `${relativePath}/`;
242
- }
243
-
244
- function summarizeByKey(
245
- entrySizes: ArchiveSizeBreakdownRow[],
246
- keyForEntry: (relativePath: string) => string | undefined,
247
- limit = 7,
248
- ): ArchiveSizeBreakdownRow[] {
249
- const totals = new Map<string, number>();
250
- for (const entry of entrySizes) {
251
- const key = keyForEntry(entry.relativePath);
252
- if (!key) continue;
253
- totals.set(key, (totals.get(key) ?? 0) + entry.bytes);
254
- }
255
- return [...totals.entries()]
256
- .map(([relativePath, bytes]) => ({ relativePath, bytes }))
257
- .sort((left, right) => right.bytes - left.bytes || left.relativePath.localeCompare(right.relativePath))
258
- .slice(0, limit);
259
- }
260
-
261
- function summarizeTopLevelIncludedPaths(entrySizes: ArchiveSizeBreakdownRow[]): ArchiveSizeBreakdownRow[] {
262
- return summarizeByKey(entrySizes, (relativePath) => {
263
- const [topLevel, ...rest] = relativePath.split("/").filter(Boolean);
264
- if (!topLevel) return undefined;
265
- return rest.length > 0 ? `${topLevel}/` : topLevel;
266
- });
267
- }
268
-
269
- function getAdaptivePrunePrefix(relativePath: string): string | undefined {
270
- const segments = relativePath.split("/").filter(Boolean);
271
- for (let index = 0; index < segments.length - 1; index += 1) {
272
- const name = segments[index];
273
- if (!ADAPTIVE_ARCHIVE_PRUNE_DIR_NAMES_ANYWHERE.has(name)) continue;
274
- const ancestors = segments.slice(0, index);
275
- if (ancestors.some((segment) => ADAPTIVE_ARCHIVE_PRUNE_PROTECTED_ANCESTOR_DIR_NAMES.has(segment))) continue;
276
- return segments.slice(0, index + 1).join("/");
277
- }
278
- return undefined;
279
- }
280
-
281
- function summarizeAdaptivePruneCandidates(
282
- entrySizes: ArchiveSizeBreakdownRow[],
283
- minimumBytes = 0,
284
- ): ArchiveSizeBreakdownRow[] {
285
- return summarizeByKey(entrySizes, getAdaptivePrunePrefix, Number.POSITIVE_INFINITY).filter((entry) => entry.bytes >= minimumBytes);
286
- }
287
-
288
- function pruneEntriesByPrefix(entries: string[], prefix: string): string[] {
289
- return entries.filter((entry) => entry !== prefix && !entry.startsWith(`${prefix}/`));
290
- }
291
-
292
- function shouldExcludeArchivePath(relativePath: string, isDirectory: boolean, options?: { forceInclude?: boolean }): boolean {
293
- const normalized = posix.normalize(relativePath).replace(/^\.\//, "");
294
- if (!normalized || normalized === ".") return false;
295
- if (options?.forceInclude) return false;
296
- const name = basename(normalized);
297
- if (DEFAULT_ARCHIVE_EXCLUDED_PATH_SEQUENCES.some((sequence) => pathContainsSequence(normalized, sequence))) return true;
298
- if (isDirectory) {
299
- if (DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE.has(name)) return true;
300
- if (getRelativeDepth(normalized) === 1 && DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_AT_REPO_ROOT.has(name)) return true;
301
- return false;
302
- }
303
- if (DEFAULT_ARCHIVE_EXCLUDED_FILES.has(name)) return true;
304
- if (name.startsWith(".env.") && !DEFAULT_ARCHIVE_EXCLUDED_ENV_ALLOWLIST.has(name)) return true;
305
- if (DEFAULT_ARCHIVE_EXCLUDED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return true;
306
- if (DEFAULT_ARCHIVE_EXCLUDED_SUBSTRINGS.some((needle) => name.includes(needle))) return true;
307
- return false;
308
- }
309
-
310
- async function isSymlinkToDirectory(path: string): Promise<boolean> {
311
- try {
312
- return (await stat(path)).isDirectory();
313
- } catch {
314
- return false;
315
- }
316
- }
317
-
318
- async function shouldExcludeArchiveChild(
319
- absolutePath: string,
320
- relativePath: string,
321
- child: { isDirectory(): boolean; isSymbolicLink(): boolean },
322
- options?: { forceInclude?: boolean },
323
- ): Promise<boolean> {
324
- const isDirectoryLike = child.isDirectory() || (child.isSymbolicLink() && await isSymlinkToDirectory(absolutePath));
325
- return shouldExcludeArchivePath(relativePath, isDirectoryLike, options);
326
- }
327
-
328
- async function expandArchiveEntries(cwd: string, relativePath: string, options?: { forceIncludeSubtree?: boolean }): Promise<string[]> {
329
- const normalized = posix.normalize(relativePath).replace(/^\.\//, "");
330
- if (normalized === ".") {
331
- const children = await readdir(cwd, { withFileTypes: true });
332
- const results: string[] = [];
333
- for (const child of children.sort((a, b) => a.name.localeCompare(b.name))) {
334
- const childRelative = child.name;
335
- if (await shouldExcludeArchiveChild(join(cwd, childRelative), childRelative, child)) continue;
336
- if (child.isDirectory()) appendArchiveEntries(results, await expandArchiveEntries(cwd, childRelative));
337
- else results.push(childRelative);
338
- }
339
- return results;
340
- }
341
-
342
- const absolute = join(cwd, normalized);
343
- const entry = await lstat(absolute);
344
- if (!entry.isDirectory()) return [normalized];
345
- if (shouldExcludeArchivePath(normalized, true, { forceInclude: options?.forceIncludeSubtree })) return [];
346
-
347
- const children = await readdir(absolute, { withFileTypes: true });
348
- const results: string[] = [];
349
- for (const child of children.sort((a, b) => a.name.localeCompare(b.name))) {
350
- const childRelative = posix.join(normalized, child.name);
351
- if (await shouldExcludeArchiveChild(join(cwd, childRelative), childRelative, child, { forceInclude: options?.forceIncludeSubtree })) continue;
352
- if (child.isDirectory()) appendArchiveEntries(results, await expandArchiveEntries(cwd, childRelative, { forceIncludeSubtree: options?.forceIncludeSubtree }));
353
- else results.push(childRelative);
354
- }
355
- return results;
356
- }
357
-
358
- async function resolveExpandedArchiveEntriesFromInputs(
359
- cwd: string,
360
- entries: Array<{ absolute: string; relative: string }>,
361
- ): Promise<string[]> {
362
- const expandedGroups = await Promise.all(entries.map(async (entry) => {
363
- const statEntry = await lstat(entry.absolute);
364
- const forceIncludeSubtree = statEntry.isDirectory() && entry.relative !== "." && shouldExcludeArchivePath(entry.relative, true);
365
- return expandArchiveEntries(cwd, entry.relative, { forceIncludeSubtree });
366
- }));
367
- return Array.from(new Set(mergeArchiveEntryGroups(expandedGroups))).sort();
368
- }
369
-
370
- export async function resolveExpandedArchiveEntries(cwd: string, files: string[]): Promise<string[]> {
371
- return resolveExpandedArchiveEntriesFromInputs(cwd, resolveArchiveInputs(cwd, files));
372
- }
373
-
374
- function isWholeRepoArchiveSelection(entries: Array<{ absolute: string; relative: string }>): boolean {
375
- return entries.length === 1 && entries[0]?.relative === ".";
376
- }
377
-
378
- async function measureArchiveEntrySizes(cwd: string, entries: string[]): Promise<ArchiveSizeBreakdownRow[]> {
379
- return Promise.all(entries.map(async (relativePath) => ({ relativePath, bytes: (await lstat(join(cwd, relativePath))).size })));
380
- }
381
-
382
- function formatArchiveOversizeError(args: {
383
- archiveBytes: number;
384
- maxBytes: number;
385
- entrySizes: ArchiveSizeBreakdownRow[];
386
- autoPrunedPrefixes: ArchiveSizeBreakdownRow[];
387
- adaptivePruneMinBytes?: number;
388
- }): string {
389
- const topLevel = summarizeTopLevelIncludedPaths(args.entrySizes);
390
- const adaptiveCandidates = summarizeAdaptivePruneCandidates(args.entrySizes, args.adaptivePruneMinBytes).slice(0, 7);
391
- return [
392
- `Oracle archive exceeds provider upload limit (${formatBytes(args.maxBytes)}) after default exclusions${args.autoPrunedPrefixes.length > 0 ? " and automatic generic generated-output-dir pruning" : ""}.`,
393
- `The local archive measured ${formatBytes(args.archiveBytes)} (${args.archiveBytes} bytes), so submission stopped before dispatch.`,
394
- args.autoPrunedPrefixes.length > 0 ? "Automatically pruned generic generated-output paths before failing:" : undefined,
395
- ...args.autoPrunedPrefixes.map((entry) => `- ${formatDirectoryLabel(entry.relativePath)} — ${formatBytes(entry.bytes)}`),
396
- topLevel.length > 0 ? "Approx top-level included sizes:" : undefined,
397
- ...topLevel.map((entry) => `- ${entry.relativePath} — ${formatBytes(entry.bytes)}`),
398
- adaptiveCandidates.length > 0 ? "Largest remaining generic generated-output-dir candidates:" : undefined,
399
- ...adaptiveCandidates.map((entry) => `- ${formatDirectoryLabel(entry.relativePath)} — ${formatBytes(entry.bytes)}`),
400
- "Recommended retry order: (1) remove the largest obviously irrelevant/generated/history/export content, (2) if it still does not fit, keep only the directly relevant subtrees plus adjacent docs/tests/config, (3) if it still does not fit, explain what was cut before asking the user.",
401
- ]
402
- .filter(Boolean)
403
- .join("\n");
404
- }
405
-
406
- function writeOctal(value: number, width: number): Buffer {
407
- const text = Math.max(0, Math.floor(value)).toString(8).slice(-(width - 1)).padStart(width - 1, "0") + "\0";
408
- return Buffer.from(text, "ascii");
409
- }
410
-
411
- function writeTarName(header: Buffer, name: string): void {
412
- const normalized = name.replaceAll("\\", "/");
413
- const nameBytes = Buffer.byteLength(normalized);
414
- if (nameBytes <= 100) {
415
- header.write(normalized, 0, 100, "utf8");
416
- return;
417
- }
418
- const parts = normalized.split("/");
419
- const fileName = parts.pop() || "";
420
- const prefix = parts.join("/");
421
- if (Buffer.byteLength(fileName) > 100 || Buffer.byteLength(prefix) > 155) {
422
- throw new Error(`archive path is too long for portable tar header: ${normalized}`);
423
- }
424
- header.write(fileName, 0, 100, "utf8");
425
- header.write(prefix, 345, 155, "utf8");
426
- }
427
-
428
- function buildTarHeader(name: string, options: { mode: number; size: number; mtimeMs: number; type: "file" | "directory" | "symlink"; linkName?: string }): Buffer {
429
- const header = Buffer.alloc(512);
430
- writeTarName(header, options.type === "directory" && !name.endsWith("/") ? `${name}/` : name);
431
- writeOctal(options.mode & 0o7777, 8).copy(header, 100);
432
- writeOctal(0, 8).copy(header, 108);
433
- writeOctal(0, 8).copy(header, 116);
434
- writeOctal(options.size, 12).copy(header, 124);
435
- writeOctal(Math.floor(options.mtimeMs / 1000), 12).copy(header, 136);
436
- Buffer.from(" ", "ascii").copy(header, 148);
437
- header[156] = options.type === "directory" ? 53 : options.type === "symlink" ? 50 : 48;
438
- if (options.linkName) header.write(options.linkName.replaceAll("\\", "/"), 157, 100, "utf8");
439
- header.write("ustar", 257, 6, "ascii");
440
- header.write("00", 263, 2, "ascii");
441
- let checksum = 0;
442
- for (const byte of header) checksum += byte;
443
- const checksumText = checksum.toString(8).padStart(6, "0");
444
- header.write(`${checksumText}\0 `, 148, 8, "ascii");
445
- return header;
446
- }
447
-
448
- async function writeChunk(stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> {
449
- if (!stream.write(chunk)) await once(stream, "drain");
450
- }
451
-
452
- async function writeWindowsTarArchiveToZstd(cwd: string, entries: string[], archivePath: string, timeout: AbortSignal): Promise<void> {
453
- const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv(process.env);
454
- const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
455
- cwd,
456
- env: scrubbedEnv,
457
- stdio: ["pipe", "ignore", "pipe"],
458
- signal: timeout,
459
- });
460
- let stderr = "";
461
- zstd.stderr?.on("data", (chunk) => {
462
- stderr += String(chunk);
463
- });
464
- try {
465
- for (const entry of entries) {
466
- const normalizedEntry = entry.replaceAll("\\", "/");
467
- const absolutePath = join(cwd, normalizedEntry);
468
- const info = await lstat(absolutePath);
469
- if (info.isSymbolicLink()) {
470
- await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "symlink", linkName: await readlink(absolutePath) }));
471
- continue;
472
- }
473
- if (info.isDirectory()) {
474
- await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "directory" }));
475
- continue;
476
- }
477
- if (!info.isFile()) continue;
478
- await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: info.size, mtimeMs: info.mtimeMs, type: "file" }));
479
- for await (const chunk of createReadStream(absolutePath)) {
480
- await writeChunk(zstd.stdin, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
481
- }
482
- const padding = info.size % 512 === 0 ? 0 : 512 - (info.size % 512);
483
- if (padding > 0) await writeChunk(zstd.stdin, Buffer.alloc(padding));
484
- }
485
- await writeChunk(zstd.stdin, Buffer.alloc(1024));
486
- zstd.stdin.end();
487
- } catch (error) {
488
- zstd.stdin.destroy();
489
- zstd.kill();
490
- throw error;
491
- }
492
- const code = await new Promise<number | null>((resolve, reject) => {
493
- zstd.once("error", reject);
494
- zstd.once("close", resolve);
495
- });
496
- if (code !== 0) throw new Error(`zstd archive compression failed with status ${code}: ${stderr.trim()}`);
497
- }
498
-
499
- async function writeArchiveFile(
500
- cwd: string,
501
- entries: string[],
502
- archivePath: string,
503
- listPath: string,
504
- options?: { commandTimeoutMs?: number },
505
- ): Promise<number> {
506
- await writeFile(listPath, Buffer.from(`${entries.join("\0")}\0`), { mode: 0o600 });
507
- await rm(archivePath, { force: true }).catch(() => undefined);
508
-
509
- if (process.platform === "win32") {
510
- const timeoutController = new AbortController();
511
- const timeout = setTimeout(() => timeoutController.abort(), options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS);
512
- try {
513
- await writeWindowsTarArchiveToZstd(cwd, entries, archivePath, timeoutController.signal);
514
- return (await stat(archivePath)).size;
515
- } catch (error) {
516
- if (timeoutController.signal.aborted) {
517
- throw new Error(`Oracle archive subprocess timed out after ${options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS}ms`);
518
- }
519
- throw error;
520
- } finally {
521
- clearTimeout(timeout);
522
- }
523
- }
524
-
525
- await new Promise<void>((resolvePromise, rejectPromise) => {
526
- const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv();
527
- const tarArgs = ["--null", "-cf", "-", "-C", cwd, "-T", basename(listPath)];
528
- const tar = spawn(process.env.PI_ORACLE_TEST_TAR_BIN ?? "tar", tarArgs, {
529
- cwd: dirname(listPath),
530
- env: scrubbedEnv,
531
- stdio: ["ignore", "pipe", "pipe"],
532
- });
533
- const zstd = spawn(process.env.PI_ORACLE_TEST_ZSTD_BIN ?? "zstd", ["-19", "-T0", "-f", "-o", archivePath], {
534
- env: scrubbedEnv,
535
- stdio: ["pipe", "ignore", "pipe"],
536
- });
537
-
538
- let stderr = "";
539
- let settled = false;
540
- let timedOut = false;
541
- let timeout: NodeJS.Timeout | undefined;
542
- let killGraceTimer: NodeJS.Timeout | undefined;
543
- let tarCode: number | null | undefined;
544
- let zstdCode: number | null | undefined;
545
-
546
- const clearTimers = () => {
547
- if (timeout) clearTimeout(timeout);
548
- if (killGraceTimer) clearTimeout(killGraceTimer);
549
- };
550
-
551
- const terminateChildren = () => {
552
- tar.kill("SIGTERM");
553
- zstd.kill("SIGTERM");
554
- killGraceTimer = setTimeout(() => {
555
- tar.kill("SIGKILL");
556
- zstd.kill("SIGKILL");
557
- }, ARCHIVE_COMMAND_KILL_GRACE_MS);
558
- killGraceTimer.unref?.();
559
- };
560
-
561
- const finish = (error?: Error) => {
562
- if (settled) return;
563
- if (error) {
564
- settled = true;
565
- clearTimers();
566
- terminateChildren();
567
- rejectPromise(error);
568
- return;
569
- }
570
- if (tarCode === undefined || zstdCode === undefined) return;
571
- settled = true;
572
- clearTimers();
573
- if (timedOut) {
574
- rejectPromise(new Error(stderr || `Oracle archive subprocess timed out after ${options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS}ms`));
575
- return;
576
- }
577
- if (tarCode === 0 && zstdCode === 0) resolvePromise();
578
- else rejectPromise(new Error(stderr || `archive command failed (tar=${tarCode}, zstd=${zstdCode})`));
579
- };
580
-
581
- const handlePipeError = (error: unknown) => {
582
- const normalized = error instanceof Error ? error : new Error(String(error));
583
- if (ARCHIVE_PIPE_FAILURE_ERROR_CODES.has(getErrorCode(normalized) ?? "")) {
584
- stderr = `${stderr}${stderr ? "\n" : ""}${normalized.message}`;
585
- tar.stdout.unpipe(zstd.stdin);
586
- terminateChildren();
587
- finish();
588
- return;
589
- }
590
- finish(normalized);
591
- };
592
-
593
- const commandTimeoutMs = options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS;
594
- if (commandTimeoutMs > 0) {
595
- timeout = setTimeout(() => {
596
- timedOut = true;
597
- stderr = `${stderr}${stderr ? "\n" : ""}Oracle archive subprocess timed out after ${commandTimeoutMs}ms`;
598
- terminateChildren();
599
- }, commandTimeoutMs);
600
- timeout.unref?.();
601
- }
602
-
603
- tar.stderr.on("data", (data) => {
604
- stderr += String(data);
605
- });
606
- zstd.stderr.on("data", (data) => {
607
- stderr += String(data);
608
- });
609
- tar.on("error", (error) => finish(error instanceof Error ? error : new Error(String(error))));
610
- zstd.on("error", (error) => finish(error instanceof Error ? error : new Error(String(error))));
611
- tar.stdout.on("error", handlePipeError);
612
- zstd.stdin.on("error", handlePipeError);
613
- tar.on("close", (code) => {
614
- tarCode = code;
615
- finish();
616
- });
617
- zstd.on("close", (code) => {
618
- zstdCode = code;
619
- finish();
620
- });
621
- tar.stdout.pipe(zstd.stdin);
622
- });
623
-
624
- return (await stat(archivePath)).size;
625
- }
626
-
627
- export async function createArchiveForTesting(
628
- cwd: string,
629
- files: string[],
630
- archivePath: string,
631
- options?: { maxBytes?: number; adaptivePruneMinBytes?: number; commandTimeoutMs?: number },
632
- ): Promise<ArchiveCreationResult> {
633
- const archiveInputs = resolveArchiveInputs(cwd, files);
634
- const wholeRepoSelection = isWholeRepoArchiveSelection(archiveInputs);
635
- let expandedEntries = await resolveExpandedArchiveEntriesFromInputs(cwd, archiveInputs);
636
- if (expandedEntries.length === 0) {
637
- throw new Error("Oracle archive inputs are empty after default exclusions");
638
- }
639
-
640
- const listDir = await mkdtemp(join(tmpdir(), "oracle-filelist-"));
641
- const listPath = join(listDir, "files.list");
642
- const maxBytes = options?.maxBytes ?? MAX_ARCHIVE_BYTES;
643
- const adaptivePruneMinBytes = options?.adaptivePruneMinBytes ?? 0;
644
- const autoPrunedPrefixes: ArchiveSizeBreakdownRow[] = [];
645
- let initialArchiveBytes: number | undefined;
646
-
647
- try {
648
- while (true) {
649
- if (expandedEntries.length === 0) {
650
- throw new Error("Oracle archive inputs are empty after default exclusions and automatic size pruning");
651
- }
652
-
653
- const archiveBytes = await writeArchiveFile(cwd, expandedEntries, archivePath, listPath, { commandTimeoutMs: options?.commandTimeoutMs });
654
- if (archiveBytes <= maxBytes) {
655
- return {
656
- sha256: await sha256File(archivePath),
657
- archiveBytes,
658
- initialArchiveBytes,
659
- autoPrunedPrefixes,
660
- includedEntries: [...expandedEntries],
661
- };
662
- }
663
-
664
- if (initialArchiveBytes === undefined) initialArchiveBytes = archiveBytes;
665
- const entrySizes = await measureArchiveEntrySizes(cwd, expandedEntries);
666
- if (!wholeRepoSelection) {
667
- throw new Error(formatArchiveOversizeError({ archiveBytes, maxBytes, entrySizes, autoPrunedPrefixes, adaptivePruneMinBytes }));
668
- }
669
-
670
- const nextCandidate = summarizeAdaptivePruneCandidates(entrySizes, adaptivePruneMinBytes).find(
671
- (entry) => !autoPrunedPrefixes.some((pruned) => pruned.relativePath === entry.relativePath),
672
- );
673
- if (!nextCandidate) {
674
- throw new Error(formatArchiveOversizeError({ archiveBytes, maxBytes, entrySizes, autoPrunedPrefixes, adaptivePruneMinBytes }));
675
- }
676
-
677
- autoPrunedPrefixes.push(nextCandidate);
678
- expandedEntries = pruneEntriesByPrefix(expandedEntries, nextCandidate.relativePath);
679
- }
680
- } finally {
681
- await rm(listDir, { recursive: true, force: true }).catch(() => undefined);
682
- }
683
- }
684
-
685
- async function createArchive(cwd: string, files: string[], archivePath: string, maxBytes = MAX_ARCHIVE_BYTES): Promise<ArchiveCreationResult> {
686
- return createArchiveForTesting(cwd, files, archivePath, { maxBytes });
687
- }
688
-
125
+ const MAX_QUEUED_ARCHIVE_BYTES_PER_ACTIVE_RUNTIME = resolveOracleProviderArchivePlan("chatgpt").maxArchiveBytes;
689
126
  function normalizeOracleProvider(value: unknown, fallback: OracleProvider, toolName = "oracle_submit"): OracleProvider {
690
127
  if (value === undefined) return fallback;
691
128
  if (typeof value !== "string") throw new Error(`${toolName} provider must be a string`);
@@ -703,10 +140,6 @@ function normalizeGrokMode(value: unknown, fallback: "heavy"): "heavy" {
703
140
  throw new Error(`Unknown Grok oracle mode: ${value}. Only heavy is currently supported.`);
704
141
  }
705
142
 
706
- function getProviderMaxArchiveBytes(provider: "chatgpt" | "grok"): number {
707
- return provider === "grok" ? GROK_MAX_ARCHIVE_BYTES : CHATGPT_MAX_ARCHIVE_BYTES;
708
- }
709
-
710
143
  export interface QueuedArchivePressure {
711
144
  queuedJobs: number;
712
145
  queuedArchiveBytes: number;
@@ -1308,7 +741,7 @@ async function runOraclePreflight(ctx: ExtensionContext, params: { provider?: un
1308
741
  }
1309
742
 
1310
743
  try {
1311
- await assertOracleSubmitPrerequisites(config);
744
+ await assertOracleSubmitPrerequisites(config, provider);
1312
745
  } catch (error) {
1313
746
  const errorDetails = buildOracleToolErrorDetails("oracle_preflight", error, asRecord(params) ?? {});
1314
747
  return {
@@ -1452,7 +885,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1452
885
  const targetChatUrl = target.chatUrl;
1453
886
  // Validate caller-specified archive paths before surfacing unrelated local setup failures such as a missing auth seed profile.
1454
887
  resolveArchiveInputs(projectCwd, params.files);
1455
- await assertOracleSubmitPrerequisites(config);
888
+ await assertOracleSubmitPrerequisites(config, provider);
1456
889
  try {
1457
890
  await withGlobalReconcileLock({ processPid: process.pid, source: "oracle_submit", cwd: projectCwd }, async () => {
1458
891
  await reconcileStaleOracleJobs();
@@ -1463,7 +896,8 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1463
896
  }
1464
897
 
1465
898
  const jobId = randomUUID();
1466
- const tempArchivePath = join(tmpdir(), `oracle-archive-${jobId}.tar.zst`);
899
+ const archivePlan = resolveOracleProviderArchivePlan(selection.provider);
900
+ const tempArchivePath = join(tmpdir(), `oracle-archive-${jobId}.${archivePlan.archiveExtension}`);
1467
901
  const runtime = allocateRuntime(config);
1468
902
  let job: OracleJob | undefined;
1469
903
  let archive: ArchiveCreationResult | undefined;
@@ -1475,7 +909,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1475
909
  let spawnedWorker: Awaited<ReturnType<typeof spawnWorker>> | undefined;
1476
910
 
1477
911
  try {
1478
- archive = await createArchive(projectCwd, params.files, tempArchivePath, getProviderMaxArchiveBytes(selection.provider));
912
+ archive = await createArchive(projectCwd, params.files, tempArchivePath, archivePlan.maxArchiveBytes, archivePlan.archiveFormat);
1479
913
  const currentArchive = archive;
1480
914
  await withLock("admission", "global", { jobId, processPid: process.pid }, async () => {
1481
915
  await promoteQueuedJobsWithinAdmissionLock({ workerPath, source: "oracle_submit" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-oracle",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "ChatGPT and Grok web-oracle extension for pi with isolated browser auth, async jobs, and project-context archives.",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -80,8 +80,8 @@
80
80
  "protobufjs": "7.6.1"
81
81
  },
82
82
  "devDependencies": {
83
- "@earendil-works/pi-ai": "0.79.1",
84
- "@earendil-works/pi-coding-agent": "0.79.1",
83
+ "@earendil-works/pi-ai": "0.79.4",
84
+ "@earendil-works/pi-coding-agent": "0.79.4",
85
85
  "@types/node": "^22.19.19",
86
86
  "esbuild": "^0.28.0",
87
87
  "tsx": "^4.22.3",
@@ -40,7 +40,7 @@ export default {
40
40
  nodeValidationMajor: 24,
41
41
  realSmoke: {
42
42
  defaultProvider: "zai",
43
- defaultModel: "glm-5.1",
43
+ defaultModel: "glm-5.2",
44
44
  authEnvByProvider: {
45
45
  zai: ["ZAI_API_KEY"],
46
46
  openai: ["OPENAI_API_KEY"],