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,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>;
@@ -0,0 +1,381 @@
1
+ // Purpose: Provide shared atomic lock/lease state helpers for oracle coordination across extension and worker processes.
2
+ // Responsibilities: Create lock/lease directories atomically, publish metadata safely, reclaim stale incomplete state, and enumerate lease metadata.
3
+ // Scope: Filesystem-backed concurrency primitives only; higher-level admission and queue behavior stays in wrapper modules.
4
+ // Usage: Imported by lib/locks.ts and worker/state-locks.mjs so both layers share identical crash-recovery semantics.
5
+ // Invariants/Assumptions: State lives under a private per-machine directory, and final published state dirs must never appear without complete metadata.
6
+
7
+ import { createHash } from "node:crypto";
8
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
10
+ import { basename, join } from "node:path";
11
+ import { isProcessAlive } from "./process-helpers.mjs";
12
+
13
+ const DEFAULT_WAIT_MS = 30_000;
14
+ const POLL_MS = 200;
15
+ export const ORACLE_METADATA_WRITE_GRACE_MS = 1_000;
16
+ /** Incomplete `.tmp-*` dirs are in-flight atomic creates; a 1s grace is too short under multi-process sweep + slow FS. */
17
+ export const ORACLE_TMP_STATE_DIR_GRACE_MS = 60_000;
18
+
19
+ function sleep(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+
23
+ /**
24
+ * @param {string} path
25
+ * @returns {Promise<void>}
26
+ */
27
+ async function ensurePrivateDir(path) {
28
+ await mkdir(path, { recursive: true, mode: 0o700 });
29
+ await chmod(path, 0o700).catch(() => undefined);
30
+ }
31
+
32
+ /**
33
+ * @param {string} kind
34
+ * @param {string} key
35
+ * @returns {string}
36
+ */
37
+ export function hashOracleStateKey(kind, key) {
38
+ return `${kind}-${createHash("sha256").update(key).digest("hex").slice(0, 24)}`;
39
+ }
40
+
41
+ /**
42
+ * @param {string} stateDir
43
+ * @returns {string}
44
+ */
45
+ export function getStateLocksDir(stateDir) {
46
+ return join(stateDir, "locks");
47
+ }
48
+
49
+ /**
50
+ * @param {string} stateDir
51
+ * @returns {string}
52
+ */
53
+ export function getStateLeasesDir(stateDir) {
54
+ return join(stateDir, "leases");
55
+ }
56
+
57
+ /**
58
+ * @param {string} stateDir
59
+ * @param {string} kind
60
+ * @param {string} key
61
+ * @returns {string}
62
+ */
63
+ function lockPath(stateDir, kind, key) {
64
+ return join(getStateLocksDir(stateDir), hashOracleStateKey(kind, key));
65
+ }
66
+
67
+ /**
68
+ * @param {string} stateDir
69
+ * @param {string} kind
70
+ * @param {string} key
71
+ * @returns {string}
72
+ */
73
+ function leasePath(stateDir, kind, key) {
74
+ return join(getStateLeasesDir(stateDir), hashOracleStateKey(kind, key));
75
+ }
76
+
77
+ /**
78
+ * @param {string} path
79
+ * @returns {string}
80
+ */
81
+ function getMetadataPath(path) {
82
+ return join(path, "metadata.json");
83
+ }
84
+
85
+ /**
86
+ * @param {string} path
87
+ * @param {unknown} metadata
88
+ * @returns {Promise<void>}
89
+ */
90
+ async function writeMetadata(path, metadata) {
91
+ const targetPath = getMetadataPath(path);
92
+ const tempPath = join(path, `metadata.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
93
+ await writeFile(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
94
+ await chmod(tempPath, 0o600).catch(() => undefined);
95
+ await rename(tempPath, targetPath);
96
+ await chmod(targetPath, 0o600).catch(() => undefined);
97
+ }
98
+
99
+ /**
100
+ * @param {string} parentDir
101
+ * @param {string} finalPath
102
+ * @param {unknown} metadata
103
+ * @returns {Promise<void>}
104
+ */
105
+ async function createStateDirAtomically(parentDir, finalPath, metadata) {
106
+ const tempPath = join(parentDir, `.tmp-${basename(finalPath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`);
107
+ await mkdir(tempPath, { recursive: false, mode: 0o700 });
108
+ try {
109
+ await writeMetadata(tempPath, metadata);
110
+ await rename(tempPath, finalPath);
111
+ } catch (error) {
112
+ await rm(tempPath, { recursive: true, force: true }).catch(() => undefined);
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {string} path
119
+ * @returns {"present" | "missing" | "invalid"}
120
+ */
121
+ function getMetadataState(path) {
122
+ const metadataPath = getMetadataPath(path);
123
+ if (!existsSync(metadataPath)) return "missing";
124
+ try {
125
+ JSON.parse(readFileSync(metadataPath, "utf8"));
126
+ return "present";
127
+ } catch {
128
+ return "invalid";
129
+ }
130
+ }
131
+
132
+ /**
133
+ * @param {string} path
134
+ * @param {number} [now]
135
+ * @returns {boolean}
136
+ */
137
+ function isIncompleteStateDirStale(path, now = Date.now()) {
138
+ try {
139
+ const stats = statSync(path);
140
+ const baselineMs = Math.max(stats.mtimeMs, stats.ctimeMs);
141
+ const graceMs = basename(path).startsWith(".tmp-") ? ORACLE_TMP_STATE_DIR_GRACE_MS : ORACLE_METADATA_WRITE_GRACE_MS;
142
+ return now - baselineMs >= graceMs;
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * @param {string} path
150
+ * @returns {number | undefined}
151
+ */
152
+ function readLockProcessPid(path) {
153
+ const metadataPath = getMetadataPath(path);
154
+ if (!existsSync(metadataPath)) return undefined;
155
+ try {
156
+ const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
157
+ return typeof metadata?.processPid === "number" && Number.isInteger(metadata.processPid) && metadata.processPid > 0
158
+ ? metadata.processPid
159
+ : undefined;
160
+ } catch {
161
+ return undefined;
162
+ }
163
+ }
164
+
165
+ /**
166
+ * @param {unknown} error
167
+ * @returns {boolean}
168
+ */
169
+ function isStateDirExistsError(error) {
170
+ return Boolean(error && typeof error === "object" && "code" in error && (error.code === "EEXIST" || error.code === "ENOTEMPTY"));
171
+ }
172
+
173
+ /**
174
+ * @param {string} path
175
+ * @param {number} [now]
176
+ * @returns {Promise<boolean>}
177
+ */
178
+ async function maybeReclaimIncompleteStateDir(path, now = Date.now()) {
179
+ if (getMetadataState(path) === "present") return false;
180
+ if (!isIncompleteStateDirStale(path, now)) return false;
181
+ await rm(path, { recursive: true, force: true }).catch(() => undefined);
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * @param {string} path
187
+ * @param {number} [now]
188
+ * @returns {Promise<boolean>}
189
+ */
190
+ async function maybeReclaimStaleLock(path, now = Date.now()) {
191
+ if (await maybeReclaimIncompleteStateDir(path, now)) return true;
192
+ const processPid = readLockProcessPid(path);
193
+ if (!processPid || isProcessAlive(processPid)) return false;
194
+ await rm(path, { recursive: true, force: true }).catch(() => undefined);
195
+ return true;
196
+ }
197
+
198
+ /**
199
+ * @param {string} stateDir
200
+ * @param {number} [now]
201
+ * @returns {Promise<string[]>}
202
+ */
203
+ export async function sweepStaleStateLocks(stateDir, now = Date.now()) {
204
+ const dir = getStateLocksDir(stateDir);
205
+ if (!existsSync(dir)) return [];
206
+ const removed = [];
207
+ for (const name of readdirSync(dir)) {
208
+ const path = join(dir, name);
209
+ if (await maybeReclaimStaleLock(path, now)) {
210
+ removed.push(path);
211
+ }
212
+ }
213
+ return removed;
214
+ }
215
+
216
+ /**
217
+ * @param {string} stateDir
218
+ * @param {string} kind
219
+ * @param {string} key
220
+ * @param {unknown} metadata
221
+ * @param {number} [timeoutMs]
222
+ * @returns {Promise<string>}
223
+ */
224
+ export async function acquireStateLock(stateDir, kind, key, metadata, timeoutMs = DEFAULT_WAIT_MS) {
225
+ const parentDir = getStateLocksDir(stateDir);
226
+ const path = join(parentDir, hashOracleStateKey(kind, key));
227
+ const deadline = Date.now() + timeoutMs;
228
+ await ensurePrivateDir(stateDir);
229
+ await ensurePrivateDir(parentDir);
230
+
231
+ while (Date.now() < deadline) {
232
+ try {
233
+ await createStateDirAtomically(parentDir, path, metadata);
234
+ return path;
235
+ } catch (error) {
236
+ if (!isStateDirExistsError(error)) throw error;
237
+ if (await maybeReclaimStaleLock(path)) continue;
238
+ }
239
+ await sleep(POLL_MS);
240
+ }
241
+
242
+ throw new Error(`Timed out waiting for oracle ${kind} lock: ${key}`);
243
+ }
244
+
245
+ /**
246
+ * @param {string | undefined} path
247
+ * @returns {Promise<void>}
248
+ */
249
+ export async function releaseStatePath(path) {
250
+ if (!path) return;
251
+ await rm(path, { recursive: true, force: true }).catch(() => undefined);
252
+ }
253
+
254
+ /**
255
+ * @template T
256
+ * @param {string} stateDir
257
+ * @param {string} kind
258
+ * @param {string} key
259
+ * @param {unknown} metadata
260
+ * @param {() => Promise<T>} fn
261
+ * @param {number} [timeoutMs]
262
+ * @returns {Promise<T>}
263
+ */
264
+ export async function withStateLock(stateDir, kind, key, metadata, fn, timeoutMs = DEFAULT_WAIT_MS) {
265
+ const handle = await acquireStateLock(stateDir, kind, key, metadata, timeoutMs);
266
+ try {
267
+ return await fn();
268
+ } finally {
269
+ await releaseStatePath(handle);
270
+ }
271
+ }
272
+
273
+ /**
274
+ * @param {string} stateDir
275
+ * @param {string} kind
276
+ * @param {string} key
277
+ * @param {unknown} metadata
278
+ * @param {number} [timeoutMs]
279
+ * @returns {Promise<string>}
280
+ */
281
+ export async function createStateLease(stateDir, kind, key, metadata, timeoutMs = DEFAULT_WAIT_MS) {
282
+ const parentDir = getStateLeasesDir(stateDir);
283
+ const path = join(parentDir, hashOracleStateKey(kind, key));
284
+ const deadline = Date.now() + timeoutMs;
285
+ await ensurePrivateDir(stateDir);
286
+ await ensurePrivateDir(parentDir);
287
+
288
+ while (Date.now() < deadline) {
289
+ try {
290
+ await createStateDirAtomically(parentDir, path, metadata);
291
+ return path;
292
+ } catch (error) {
293
+ if (!isStateDirExistsError(error)) throw error;
294
+ if (await maybeReclaimIncompleteStateDir(path)) continue;
295
+ if (getMetadataState(path) === "present") throw error;
296
+ }
297
+ await sleep(POLL_MS);
298
+ }
299
+
300
+ throw new Error(`Timed out waiting for oracle ${kind} lease: ${key}`);
301
+ }
302
+
303
+ /**
304
+ * @param {string} stateDir
305
+ * @param {string} kind
306
+ * @param {string} key
307
+ * @param {unknown} metadata
308
+ * @returns {Promise<string>}
309
+ */
310
+ export async function writeStateLeaseMetadata(stateDir, kind, key, metadata) {
311
+ const parentDir = getStateLeasesDir(stateDir);
312
+ const path = join(parentDir, hashOracleStateKey(kind, key));
313
+ await ensurePrivateDir(stateDir);
314
+ await ensurePrivateDir(parentDir);
315
+ if (existsSync(path)) {
316
+ await chmod(path, 0o700).catch(() => undefined);
317
+ await writeMetadata(path, metadata);
318
+ return path;
319
+ }
320
+ try {
321
+ await createStateDirAtomically(parentDir, path, metadata);
322
+ } catch (error) {
323
+ if (!isStateDirExistsError(error)) throw error;
324
+ if (await maybeReclaimIncompleteStateDir(path)) {
325
+ await createStateDirAtomically(parentDir, path, metadata);
326
+ } else {
327
+ await writeMetadata(path, metadata);
328
+ }
329
+ }
330
+ return path;
331
+ }
332
+
333
+ /**
334
+ * @template T
335
+ * @param {string} stateDir
336
+ * @param {string} kind
337
+ * @param {string} key
338
+ * @returns {Promise<T | undefined>}
339
+ */
340
+ export async function readStateLeaseMetadata(stateDir, kind, key) {
341
+ const path = getMetadataPath(leasePath(stateDir, kind, key));
342
+ if (!existsSync(path)) return undefined;
343
+ try {
344
+ return JSON.parse(await readFile(path, "utf8"));
345
+ } catch {
346
+ return undefined;
347
+ }
348
+ }
349
+
350
+ /**
351
+ * @template T
352
+ * @param {string} stateDir
353
+ * @param {string} kind
354
+ * @returns {T[]}
355
+ */
356
+ export function listStateLeaseMetadata(stateDir, kind) {
357
+ const dir = getStateLeasesDir(stateDir);
358
+ if (!existsSync(dir)) return [];
359
+ return readdirSync(dir)
360
+ .filter((name) => name.startsWith(`${kind}-`))
361
+ .map((name) => join(dir, name, "metadata.json"))
362
+ .filter((path) => existsSync(path))
363
+ .flatMap((path) => {
364
+ try {
365
+ return [JSON.parse(readFileSync(path, "utf8"))];
366
+ } catch {
367
+ return [];
368
+ }
369
+ });
370
+ }
371
+
372
+ /**
373
+ * @param {string} stateDir
374
+ * @param {string} kind
375
+ * @param {string | undefined} key
376
+ * @returns {Promise<void>}
377
+ */
378
+ export async function releaseStateLease(stateDir, kind, key) {
379
+ if (!key) return;
380
+ await rm(leasePath(stateDir, kind, key), { recursive: true, force: true }).catch(() => undefined);
381
+ }
@@ -1,3 +1,8 @@
1
+ // Purpose: Parse agent-browser snapshots and infer likely downloadable artifact labels from response UI structure.
2
+ // Responsibilities: Extract snapshot entries, detect candidate filenames, and partition strong versus suspicious artifact signals.
3
+ // Scope: Pure heuristic parsing only; browser interaction and artifact download orchestration stay in the worker.
4
+ // Usage: Imported by worker runtime code, UI helper modules, and sanity tests to keep artifact detection behavior deterministic.
5
+ // Invariants/Assumptions: Snapshot text comes from agent-browser `snapshot -i`, and heuristics should prefer false negatives over noisy false positives.
1
6
  export const FILE_LABEL_PATTERN_SOURCE = String.raw`(?:^|[^A-Za-z0-9._~/-])((?:(?:[A-Za-z]:)?[\\/]|[.~][\\/])?(?:[^\\/\s"'<>|]+[\\/])*[^\\/\s"'<>|]+\.[A-Za-z0-9]{1,12})(?=$|[^A-Za-z0-9._~/-])`;
2
7
  const FILE_LABEL_PATTERN = new RegExp(FILE_LABEL_PATTERN_SOURCE, "g");
3
8
  export const GENERIC_ARTIFACT_LABELS = ["ATTACHED", "DONE"];