@rudderhq/agent-runtime-codex-local 0.7.21-canary.8 → 0.7.21

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.
@@ -1,8 +1,19 @@
1
+ import { execFile } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
5
+ import { promisify } from "node:util";
4
6
  const STATE_VERSION = 1;
5
7
  const STATE_RELATIVE_DIRECTORY = path.join(".rudder", "provider-readiness", "codex");
8
+ const AUTH_FAILURE_COOLDOWN_MS = 60_000;
9
+ const PROBE_LEASE_MS = 120_000;
10
+ const STATE_LOCK_STALE_MS = 30_000;
11
+ const STATE_LOCK_WAIT_MS = 2_000;
12
+ const STATE_LOCK_RETRY_MS = 10;
13
+ const STATE_LOCK_COORDINATION_SUFFIX = ".coordination";
14
+ const PROCESS_START_LOOKUP_TIMEOUT_MS = 500;
15
+ const execFileAsync = promisify(execFile);
16
+ const activeProbeLeases = new Map();
6
17
  function digestParts(parts) {
7
18
  const hash = createHash("sha256");
8
19
  for (const part of parts) {
@@ -14,16 +25,124 @@ function digestParts(parts) {
14
25
  }
15
26
  return hash.digest("hex");
16
27
  }
28
+ async function readProcessStartIdentity(pid) {
29
+ if (!Number.isSafeInteger(pid) || pid <= 0)
30
+ return null;
31
+ if (process.platform === "linux") {
32
+ const raw = await fs.readFile(`/proc/${pid}/stat`, "utf8").catch(() => null);
33
+ if (raw) {
34
+ const closingCommand = raw.lastIndexOf(") ");
35
+ const fields = closingCommand >= 0 ? raw.slice(closingCommand + 2).trim().split(/\s+/u) : [];
36
+ // /proc fields start at field 3 after the command name. Field 22 is the
37
+ // process start time in clock ticks, which survives PID reuse.
38
+ const startTime = fields[19];
39
+ if (startTime)
40
+ return `linux:${startTime}`;
41
+ }
42
+ }
43
+ try {
44
+ const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="], { timeout: PROCESS_START_LOOKUP_TIMEOUT_MS, windowsHide: true });
45
+ const startTime = stdout.trim();
46
+ return startTime ? `ps:${startTime}` : null;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function activeProbeKey(agentHome, fingerprint) {
53
+ return statePath(agentHome, fingerprint);
54
+ }
55
+ function rememberActiveProbe(agentHome, fingerprint, probeId) {
56
+ activeProbeLeases.set(activeProbeKey(agentHome, fingerprint), {
57
+ probeId,
58
+ renewedAt: process.hrtime.bigint(),
59
+ });
60
+ }
61
+ function forgetActiveProbe(agentHome, fingerprint, probeId) {
62
+ const key = activeProbeKey(agentHome, fingerprint);
63
+ const current = activeProbeLeases.get(key);
64
+ if (!current || probeId === undefined || current.probeId === probeId) {
65
+ activeProbeLeases.delete(key);
66
+ }
67
+ }
68
+ function hasFreshLocalProbeLease(agentHome, fingerprint, probeId) {
69
+ const key = activeProbeKey(agentHome, fingerprint);
70
+ const current = activeProbeLeases.get(key);
71
+ if (!current || current.probeId !== probeId)
72
+ return false;
73
+ if (process.hrtime.bigint() - current.renewedAt >= BigInt(PROBE_LEASE_MS) * 1000000n) {
74
+ activeProbeLeases.delete(key);
75
+ return false;
76
+ }
77
+ return true;
78
+ }
17
79
  async function readFingerprintInput(candidate) {
18
- return fs.readFile(candidate).catch(() => Buffer.from("missing", "utf8"));
80
+ return fs.readFile(candidate).catch(() => Buffer.from("", "utf8"));
81
+ }
82
+ function isTomlTableBoundary(trimmedLine) {
83
+ return /^\[\[.+\]\]$/.test(trimmedLine) || /^\[(?!\[).+\]$/.test(trimmedLine);
84
+ }
85
+ function isManagedCodexConfigTable(trimmedLine) {
86
+ return /^\[mcp_servers(?:\..+)?\]$/.test(trimmedLine)
87
+ || /^\[plugins\..+\]$/.test(trimmedLine)
88
+ || trimmedLine === "[[skills.config]]";
89
+ }
90
+ function isUnsupportedServiceTierLine(trimmedLine) {
91
+ const match = trimmedLine.match(/^service_tier\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s#]+))/i);
92
+ if (!match)
93
+ return false;
94
+ const value = (match[1] ?? match[2] ?? match[3] ?? "").trim().toLowerCase();
95
+ return value !== "fast" && value !== "flex";
96
+ }
97
+ function normalizeProviderConfig(content) {
98
+ const output = [];
99
+ let blockLines = null;
100
+ let blockName = "";
101
+ const flushBlock = () => {
102
+ if (!blockLines)
103
+ return;
104
+ if (!isManagedCodexConfigTable(blockName)) {
105
+ const filtered = blockName === "[features]"
106
+ ? blockLines.filter((line, index) => index === 0 || !/^\s*plugins\s*=/.test(line))
107
+ : blockName === "[skills.bundled]"
108
+ ? blockLines.filter((line, index) => index === 0 || !/^\s*enabled\s*=/.test(line))
109
+ : blockLines;
110
+ if (filtered.slice(1).some((line) => line.trim().length > 0))
111
+ output.push(...filtered);
112
+ }
113
+ blockLines = null;
114
+ blockName = "";
115
+ };
116
+ for (const line of content.split(/\r?\n/)) {
117
+ const trimmedLine = line.trim();
118
+ if (isTomlTableBoundary(trimmedLine)) {
119
+ flushBlock();
120
+ blockLines = [line];
121
+ blockName = trimmedLine;
122
+ continue;
123
+ }
124
+ if (blockLines) {
125
+ blockLines.push(line);
126
+ continue;
127
+ }
128
+ if (trimmedLine === "# rudder-managed-skills:start" || trimmedLine === "# rudder-managed-skills:end")
129
+ continue;
130
+ if (/^\s*notify\s*=/.test(trimmedLine) || isUnsupportedServiceTierLine(trimmedLine))
131
+ continue;
132
+ output.push(line);
133
+ }
134
+ flushBlock();
135
+ return output.join("\n").trim().replace(/\n{3,}/g, "\n\n");
19
136
  }
20
137
  export async function buildCodexReadinessFingerprint(input) {
21
138
  const apiKey = input.env.OPENAI_API_KEY?.trim() ?? "";
22
139
  const authSource = apiKey ? "api_key" : "subscription";
140
+ const snapshotHome = input.codexHome ?? input.sharedCodexHome;
23
141
  const authMaterial = apiKey
24
142
  ? Buffer.from(apiKey, "utf8")
25
- : await readFingerprintInput(path.join(input.sharedCodexHome, "auth.json"));
26
- const providerConfig = await readFingerprintInput(path.join(input.sharedCodexHome, "config.toml"));
143
+ : await readFingerprintInput(path.join(snapshotHome, "auth.json"));
144
+ const rawProviderConfig = await readFingerprintInput(path.join(snapshotHome, "config.toml"));
145
+ const providerConfig = Buffer.from(normalizeProviderConfig(rawProviderConfig.toString("utf8")), "utf8");
27
146
  return digestParts([
28
147
  "rudder.codex.readiness.v1",
29
148
  authSource,
@@ -36,6 +155,9 @@ export async function buildCodexReadinessFingerprint(input) {
36
155
  function statePath(agentHome, fingerprint) {
37
156
  return path.join(agentHome, STATE_RELATIVE_DIRECTORY, `${fingerprint}.json`);
38
157
  }
158
+ function stateLockPath(agentHome, fingerprint) {
159
+ return `${statePath(agentHome, fingerprint)}.lock`;
160
+ }
39
161
  async function readState(agentHome, fingerprint) {
40
162
  const raw = await fs.readFile(statePath(agentHome, fingerprint), "utf8").catch(() => null);
41
163
  if (!raw)
@@ -43,49 +165,502 @@ async function readState(agentHome, fingerprint) {
43
165
  try {
44
166
  const parsed = JSON.parse(raw);
45
167
  if (parsed.version !== STATE_VERSION
46
- || typeof parsed.fingerprint !== "string"
168
+ || parsed.fingerprint !== fingerprint
47
169
  || parsed.classification !== "authentication"
48
- || parsed.errorCode !== "codex_provider_auth_required"
49
- || typeof parsed.failedAt !== "string") {
170
+ || parsed.errorCode !== "codex_provider_auth_required") {
50
171
  return null;
51
172
  }
52
- return parsed;
173
+ const state = parsed.state === undefined ? "failed" : parsed.state;
174
+ if (state !== "failed" && state !== "probing")
175
+ return null;
176
+ const generation = typeof parsed.generation === "number" && Number.isSafeInteger(parsed.generation)
177
+ && parsed.generation >= 0
178
+ ? parsed.generation
179
+ : 0;
180
+ if (state === "probing" && (typeof parsed.probeId !== "string" || typeof parsed.probeStartedAt !== "string")) {
181
+ return null;
182
+ }
183
+ if (state === "failed" && typeof parsed.failedAt !== "string")
184
+ return null;
185
+ return {
186
+ version: STATE_VERSION,
187
+ fingerprint: parsed.fingerprint,
188
+ classification: "authentication",
189
+ errorCode: "codex_provider_auth_required",
190
+ state,
191
+ generation,
192
+ ...(typeof parsed.failedAt === "string" ? { failedAt: parsed.failedAt } : {}),
193
+ ...(typeof parsed.probeId === "string" ? { probeId: parsed.probeId } : {}),
194
+ ...(typeof parsed.probeStartedAt === "string" ? { probeStartedAt: parsed.probeStartedAt } : {}),
195
+ ...(typeof parsed.probeOwnerPid === "number" && Number.isSafeInteger(parsed.probeOwnerPid) && parsed.probeOwnerPid > 0
196
+ ? { probeOwnerPid: parsed.probeOwnerPid }
197
+ : {}),
198
+ };
53
199
  }
54
200
  catch {
55
201
  return null;
56
202
  }
57
203
  }
58
- export async function hasMatchingCodexAuthFailure(agentHome, fingerprint) {
59
- const state = await readState(agentHome, fingerprint);
60
- return state?.fingerprint === fingerprint;
204
+ function isWithinDuration(timestamp, durationMs, nowMs) {
205
+ const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
206
+ if (!Number.isFinite(timestampMs))
207
+ return false;
208
+ const elapsedMs = nowMs - timestampMs;
209
+ // A future timestamp is treated as expired. Clock skew must not turn a
210
+ // provider failure or abandoned probe into an indefinite block.
211
+ return elapsedMs >= 0 && elapsedMs < durationMs;
61
212
  }
62
- export async function recordCodexAuthFailure(agentHome, fingerprint) {
63
- const target = statePath(agentHome, fingerprint);
64
- const directory = path.dirname(target);
65
- await fs.mkdir(directory, { recursive: true });
66
- const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
67
- const state = {
68
- version: STATE_VERSION,
69
- fingerprint,
70
- classification: "authentication",
71
- errorCode: "codex_provider_auth_required",
72
- failedAt: new Date().toISOString(),
213
+ function isActiveState(agentHome, fingerprint, state, nowMs) {
214
+ return state.state === "failed"
215
+ ? isWithinDuration(state.failedAt, AUTH_FAILURE_COOLDOWN_MS, nowMs)
216
+ : isProbeLeaseActive(agentHome, fingerprint, state, nowMs);
217
+ }
218
+ function isProbeLeaseActive(agentHome, fingerprint, state, nowMs) {
219
+ const probeStartedAtMs = typeof state.probeStartedAt === "string"
220
+ ? Date.parse(state.probeStartedAt)
221
+ : Number.NaN;
222
+ if (!Number.isFinite(probeStartedAtMs))
223
+ return false;
224
+ if (probeStartedAtMs > nowMs) {
225
+ // A wall-clock rollback cannot prove that a long-lived Rudder server still
226
+ // owns the probe. Only a bounded in-process renewal can keep this state
227
+ // active; an abandoned future-dated record is therefore recoverable.
228
+ return hasFreshLocalProbeLease(agentHome, fingerprint, state.probeId ?? "");
229
+ }
230
+ return nowMs - probeStartedAtMs < PROBE_LEASE_MS;
231
+ }
232
+ function parseStateLock(raw) {
233
+ try {
234
+ const parsed = JSON.parse(raw);
235
+ if (typeof parsed.ownerToken !== "string"
236
+ || parsed.ownerToken.length === 0
237
+ || typeof parsed.ownerPid !== "number"
238
+ || !Number.isSafeInteger(parsed.ownerPid)
239
+ || parsed.ownerPid <= 0) {
240
+ return null;
241
+ }
242
+ return {
243
+ ownerToken: parsed.ownerToken,
244
+ ownerPid: parsed.ownerPid,
245
+ ownerStartIdentity: typeof parsed.ownerStartIdentity === "string" ? parsed.ownerStartIdentity : null,
246
+ };
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ async function readStateLock(lockPath) {
253
+ const stat = await fs.stat(lockPath).catch(() => null);
254
+ if (!stat)
255
+ return null;
256
+ const raw = await fs.readFile(lockPath, "utf8").catch(() => "");
257
+ const parsed = parseStateLock(raw);
258
+ return {
259
+ raw,
260
+ ownerToken: parsed?.ownerToken ?? null,
261
+ ownerPid: parsed?.ownerPid ?? null,
262
+ ownerStartIdentity: parsed?.ownerStartIdentity ?? null,
263
+ stat,
73
264
  };
74
- await fs.writeFile(temporary, `${JSON.stringify(state)}\n`, { encoding: "utf8", mode: 0o600 });
265
+ }
266
+ function isProcessAlive(pid) {
267
+ if (pid === null)
268
+ return false;
75
269
  try {
76
- await fs.link(temporary, target);
270
+ process.kill(pid, 0);
271
+ return true;
77
272
  }
78
273
  catch (error) {
79
- if (error.code !== "EEXIST")
80
- throw error;
274
+ return error.code !== "ESRCH";
275
+ }
276
+ }
277
+ function isSameFileIdentity(left, right) {
278
+ if (left.dev !== 0 || left.ino !== 0 || right.dev !== 0 || right.ino !== 0) {
279
+ return left.dev === right.dev && left.ino === right.ino;
280
+ }
281
+ return left.size === right.size && left.mtimeMs === right.mtimeMs;
282
+ }
283
+ function isSameStateLockSnapshot(left, right) {
284
+ return left.raw === right.raw && isSameFileIdentity(left.stat, right.stat);
285
+ }
286
+ function isSameStateLockCoordinationSnapshot(left, right) {
287
+ return left.raw === right.raw && isSameFileIdentity(left.stat, right.stat);
288
+ }
289
+ async function isStateLockStale(snapshot, nowMs) {
290
+ // A live owner is allowed to renew its descriptor even when a filesystem
291
+ // clock jumps backwards or the operation is temporarily slow.
292
+ if (isProcessAlive(snapshot.ownerPid)) {
293
+ if (!snapshot.ownerStartIdentity)
294
+ return false;
295
+ const currentStartIdentity = await readProcessStartIdentity(snapshot.ownerPid);
296
+ // An unavailable process inspection is fail-closed for the lock: retain
297
+ // the lock rather than reclaiming a possibly live owner.
298
+ return currentStartIdentity !== null && currentStartIdentity !== snapshot.ownerStartIdentity;
299
+ }
300
+ const birthtimeMs = Number(snapshot.stat.birthtimeMs);
301
+ const ctimeMs = Number(snapshot.stat.ctimeMs);
302
+ const createdAtMs = Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : ctimeMs;
303
+ const ageMs = nowMs - createdAtMs;
304
+ // A dead, identified owner cannot make progress regardless of the
305
+ // filesystem timestamp. An unidentified lock uses creation time rather than
306
+ // mtime so a future-dated timestamp cannot turn stale contention indefinite.
307
+ if (snapshot.ownerPid !== null)
308
+ return true;
309
+ return ageMs >= STATE_LOCK_STALE_MS;
310
+ }
311
+ function stateLockCoordinationPath(lockPath) {
312
+ return `${lockPath}${STATE_LOCK_COORDINATION_SUFFIX}`;
313
+ }
314
+ function parseCoordinationOwner(raw) {
315
+ try {
316
+ const parsed = JSON.parse(raw);
317
+ if (typeof parsed.ownerToken !== "string"
318
+ || parsed.ownerToken.length === 0
319
+ || typeof parsed.ownerPid !== "number"
320
+ || !Number.isSafeInteger(parsed.ownerPid)
321
+ || parsed.ownerPid <= 0) {
322
+ return null;
323
+ }
324
+ return {
325
+ ownerToken: parsed.ownerToken,
326
+ ownerPid: parsed.ownerPid,
327
+ ownerStartIdentity: typeof parsed.ownerStartIdentity === "string" ? parsed.ownerStartIdentity : null,
328
+ };
329
+ }
330
+ catch {
331
+ return null;
332
+ }
333
+ }
334
+ async function readStateLockCoordination(coordinationPath) {
335
+ const stat = await fs.stat(coordinationPath).catch(() => null);
336
+ if (!stat || !stat.isDirectory())
337
+ return null;
338
+ const ownerPath = path.join(coordinationPath, "owner.json");
339
+ const raw = await fs.readFile(ownerPath, "utf8").catch(() => "");
340
+ const parsed = parseCoordinationOwner(raw);
341
+ return {
342
+ raw,
343
+ ownerToken: parsed?.ownerToken ?? null,
344
+ ownerPid: parsed?.ownerPid ?? null,
345
+ ownerStartIdentity: parsed?.ownerStartIdentity ?? null,
346
+ stat,
347
+ };
348
+ }
349
+ async function isStateLockCoordinationStale(snapshot, nowMs) {
350
+ if (isProcessAlive(snapshot.ownerPid)) {
351
+ if (!snapshot.ownerStartIdentity)
352
+ return false;
353
+ const currentStartIdentity = await readProcessStartIdentity(snapshot.ownerPid);
354
+ return currentStartIdentity !== null && currentStartIdentity !== snapshot.ownerStartIdentity;
355
+ }
356
+ const birthtimeMs = Number(snapshot.stat.birthtimeMs);
357
+ const ctimeMs = Number(snapshot.stat.ctimeMs);
358
+ const createdAtMs = Number.isFinite(birthtimeMs) && birthtimeMs > 0 ? birthtimeMs : ctimeMs;
359
+ if (snapshot.ownerPid !== null)
360
+ return true;
361
+ return nowMs - createdAtMs >= STATE_LOCK_STALE_MS;
362
+ }
363
+ async function releaseStateLockCoordination(coordination) {
364
+ const current = await readStateLockCoordination(coordination.coordinationPath);
365
+ if (!current
366
+ || current.ownerToken !== coordination.ownerToken
367
+ || current.stat.dev !== coordination.stat.dev
368
+ || current.stat.ino !== coordination.stat.ino) {
369
+ return;
370
+ }
371
+ await fs.unlink(path.join(coordination.coordinationPath, "owner.json")).catch(() => undefined);
372
+ await fs.rmdir(coordination.coordinationPath).catch(() => undefined);
373
+ }
374
+ async function acquireStateLockCoordination(lockPath) {
375
+ const coordinationPath = stateLockCoordinationPath(lockPath);
376
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
377
+ while (Date.now() <= deadline) {
378
+ try {
379
+ await fs.mkdir(coordinationPath, { recursive: false, mode: 0o700 });
380
+ const ownerToken = randomUUID();
381
+ try {
382
+ const ownerStartIdentity = await readProcessStartIdentity(process.pid);
383
+ await fs.writeFile(path.join(coordinationPath, "owner.json"), `${JSON.stringify({ ownerToken, ownerPid: process.pid, ownerStartIdentity })}\n`, { encoding: "utf8", mode: 0o600 });
384
+ return {
385
+ coordinationPath,
386
+ ownerToken,
387
+ ownerStartIdentity,
388
+ stat: await fs.stat(coordinationPath),
389
+ };
390
+ }
391
+ catch (error) {
392
+ await fs.unlink(path.join(coordinationPath, "owner.json")).catch(() => undefined);
393
+ await fs.rmdir(coordinationPath).catch(() => undefined);
394
+ throw error;
395
+ }
396
+ }
397
+ catch (error) {
398
+ if (error.code !== "EEXIST")
399
+ throw error;
400
+ const existing = await readStateLockCoordination(coordinationPath);
401
+ if (existing && await isStateLockCoordinationStale(existing, Date.now())) {
402
+ // Rename the exact stale directory into a unique tombstone before
403
+ // removing it. A second reclaimer cannot remove a replacement owner
404
+ // that has already recreated the original coordination path.
405
+ const reclaimPath = `${coordinationPath}.${process.pid}.${randomUUID()}.reclaim`;
406
+ try {
407
+ const current = await readStateLockCoordination(coordinationPath);
408
+ if (current && isSameStateLockCoordinationSnapshot(current, existing)) {
409
+ await fs.rename(coordinationPath, reclaimPath);
410
+ await fs.rm(reclaimPath, { recursive: true, force: true });
411
+ }
412
+ }
413
+ catch (reclaimError) {
414
+ const code = reclaimError.code;
415
+ if (code !== "ENOENT" && code !== "EEXIST" && code !== "ENOTEMPTY")
416
+ throw reclaimError;
417
+ }
418
+ continue;
419
+ }
420
+ await new Promise((resolve) => setTimeout(resolve, STATE_LOCK_RETRY_MS));
421
+ }
422
+ }
423
+ return null;
424
+ }
425
+ async function releaseStateLock(lock) {
426
+ await lock.handle.close().catch(() => undefined);
427
+ const current = await readStateLock(lock.lockPath);
428
+ if (!current
429
+ || current.ownerToken !== lock.ownerToken
430
+ || !isSameFileIdentity(current.stat, lock.stat)) {
431
+ return;
432
+ }
433
+ await fs.unlink(lock.lockPath).catch(() => undefined);
434
+ }
435
+ async function createStateLock(lockPath) {
436
+ const handle = await fs.open(lockPath, "wx", 0o600);
437
+ const ownerToken = randomUUID();
438
+ const ownerStartIdentity = await readProcessStartIdentity(process.pid);
439
+ try {
440
+ await handle.writeFile(`${JSON.stringify({ ownerToken, ownerPid: process.pid, ownerStartIdentity })}\n`, "utf8");
441
+ return {
442
+ handle,
443
+ lockPath,
444
+ ownerToken,
445
+ ownerStartIdentity,
446
+ stat: await handle.stat(),
447
+ };
448
+ }
449
+ catch (error) {
450
+ await handle.close().catch(() => undefined);
451
+ await fs.unlink(lockPath).catch(() => undefined);
452
+ throw error;
453
+ }
454
+ }
455
+ async function acquireStateLock(agentHome, fingerprint) {
456
+ const lockPath = stateLockPath(agentHome, fingerprint);
457
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
458
+ const deadline = Date.now() + STATE_LOCK_WAIT_MS;
459
+ while (Date.now() <= deadline) {
460
+ const coordination = await acquireStateLockCoordination(lockPath);
461
+ if (!coordination)
462
+ return null;
463
+ let lock = null;
464
+ try {
465
+ try {
466
+ lock = await createStateLock(lockPath);
467
+ }
468
+ catch (error) {
469
+ if (error.code !== "EEXIST")
470
+ throw error;
471
+ const existing = await readStateLock(lockPath);
472
+ if (existing && await isStateLockStale(existing, Date.now())) {
473
+ // Every claimant holds the coordination directory while creating or
474
+ // reclaiming the state lock, so stale deletion cannot race a normal
475
+ // create or another stale takeover.
476
+ await fs.unlink(lockPath).catch((unlinkError) => {
477
+ if (unlinkError.code !== "ENOENT")
478
+ throw unlinkError;
479
+ });
480
+ try {
481
+ lock = await createStateLock(lockPath);
482
+ }
483
+ catch (retryError) {
484
+ if (retryError.code !== "EEXIST")
485
+ throw retryError;
486
+ }
487
+ }
488
+ }
489
+ }
490
+ finally {
491
+ await releaseStateLockCoordination(coordination);
492
+ }
493
+ if (lock)
494
+ return lock;
495
+ await new Promise((resolve) => setTimeout(resolve, STATE_LOCK_RETRY_MS));
496
+ }
497
+ return null;
498
+ }
499
+ async function withStateLock(agentHome, fingerprint, fn) {
500
+ const lockPath = stateLockPath(agentHome, fingerprint);
501
+ const lock = await acquireStateLock(agentHome, fingerprint);
502
+ if (!lock)
503
+ return null;
504
+ const refreshInterval = setInterval(() => {
505
+ void lock.handle.utimes(new Date(), new Date()).catch(() => undefined);
506
+ }, Math.max(1_000, Math.floor(STATE_LOCK_STALE_MS / 3)));
507
+ refreshInterval.unref?.();
508
+ try {
509
+ return await fn();
510
+ }
511
+ finally {
512
+ clearInterval(refreshInterval);
513
+ await releaseStateLock(lock);
514
+ }
515
+ }
516
+ function isStateRenameCollision(error) {
517
+ if (!(error instanceof Error))
518
+ return false;
519
+ const code = error.code;
520
+ return code === "EEXIST" || code === "ENOTEMPTY" || code === "EPERM";
521
+ }
522
+ async function writeState(agentHome, fingerprint, state) {
523
+ const target = statePath(agentHome, fingerprint);
524
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
525
+ await fs.writeFile(temporary, `${JSON.stringify(state)}\n`, { encoding: "utf8", mode: 0o600 });
526
+ try {
527
+ try {
528
+ await fs.rename(temporary, target);
529
+ }
530
+ catch (error) {
531
+ if (!isStateRenameCollision(error))
532
+ throw error;
533
+ await fs.unlink(target).catch(() => undefined);
534
+ await fs.rename(temporary, target);
535
+ }
81
536
  }
82
537
  finally {
83
538
  await fs.unlink(temporary).catch(() => undefined);
84
539
  }
85
540
  }
86
- export async function clearMatchingCodexAuthFailure(agentHome, fingerprint) {
87
- if (!(await hasMatchingCodexAuthFailure(agentHome, fingerprint)))
88
- return;
89
- await fs.unlink(statePath(agentHome, fingerprint)).catch(() => undefined);
541
+ function nextGeneration(state) {
542
+ return (state?.generation ?? 0) + 1;
543
+ }
544
+ function matchesProbe(state, lease) {
545
+ return state?.state === "probing"
546
+ && state.probeId === lease.probeId
547
+ && state.generation === lease.generation;
548
+ }
549
+ export async function claimCodexAuthProbe(agentHome, fingerprint) {
550
+ const claim = await withStateLock(agentHome, fingerprint, async () => {
551
+ const current = await readState(agentHome, fingerprint);
552
+ const now = Date.now();
553
+ if (current && isActiveState(agentHome, fingerprint, current, now)) {
554
+ return { claimed: false, readinessState: "unchanged" };
555
+ }
556
+ const lease = {
557
+ probeId: randomUUID(),
558
+ generation: nextGeneration(current),
559
+ };
560
+ await writeState(agentHome, fingerprint, {
561
+ version: STATE_VERSION,
562
+ fingerprint,
563
+ classification: "authentication",
564
+ errorCode: "codex_provider_auth_required",
565
+ state: "probing",
566
+ generation: lease.generation,
567
+ probeId: lease.probeId,
568
+ probeStartedAt: new Date(now).toISOString(),
569
+ probeOwnerPid: process.pid,
570
+ });
571
+ rememberActiveProbe(agentHome, fingerprint, lease.probeId);
572
+ return { claimed: true, lease };
573
+ });
574
+ return claim ?? { claimed: false, readinessState: "busy" };
575
+ }
576
+ export async function hasMatchingCodexAuthFailure(agentHome, fingerprint) {
577
+ const state = await readState(agentHome, fingerprint);
578
+ return state?.fingerprint === fingerprint
579
+ && isActiveState(agentHome, fingerprint, state, Date.now());
580
+ }
581
+ export async function recordCodexAuthFailure(agentHome, fingerprint, lease) {
582
+ const recorded = await withStateLock(agentHome, fingerprint, async () => {
583
+ const current = await readState(agentHome, fingerprint);
584
+ const now = Date.now();
585
+ if (lease) {
586
+ if (!matchesProbe(current, lease)) {
587
+ forgetActiveProbe(agentHome, fingerprint, lease.probeId);
588
+ return false;
589
+ }
590
+ await writeState(agentHome, fingerprint, {
591
+ version: STATE_VERSION,
592
+ fingerprint,
593
+ classification: "authentication",
594
+ errorCode: "codex_provider_auth_required",
595
+ state: "failed",
596
+ generation: lease.generation,
597
+ failedAt: new Date(now).toISOString(),
598
+ });
599
+ forgetActiveProbe(agentHome, fingerprint, lease.probeId);
600
+ return true;
601
+ }
602
+ // Keep the compatibility API fail-closed: an unowned caller cannot steal
603
+ // an active probe belonging to another run.
604
+ if (current && isActiveState(agentHome, fingerprint, current, now))
605
+ return false;
606
+ await writeState(agentHome, fingerprint, {
607
+ version: STATE_VERSION,
608
+ fingerprint,
609
+ classification: "authentication",
610
+ errorCode: "codex_provider_auth_required",
611
+ state: "failed",
612
+ generation: nextGeneration(current),
613
+ failedAt: new Date(now).toISOString(),
614
+ });
615
+ return true;
616
+ });
617
+ return recorded ?? false;
618
+ }
619
+ export async function renewCodexAuthProbe(agentHome, fingerprint, lease) {
620
+ const renewed = await withStateLock(agentHome, fingerprint, async () => {
621
+ const current = await readState(agentHome, fingerprint);
622
+ if (!matchesProbe(current, lease)) {
623
+ forgetActiveProbe(agentHome, fingerprint, lease.probeId);
624
+ return false;
625
+ }
626
+ await writeState(agentHome, fingerprint, {
627
+ version: STATE_VERSION,
628
+ fingerprint,
629
+ classification: "authentication",
630
+ errorCode: "codex_provider_auth_required",
631
+ state: "probing",
632
+ generation: lease.generation,
633
+ probeId: lease.probeId,
634
+ probeStartedAt: new Date().toISOString(),
635
+ probeOwnerPid: process.pid,
636
+ });
637
+ rememberActiveProbe(agentHome, fingerprint, lease.probeId);
638
+ return true;
639
+ });
640
+ return renewed ?? false;
641
+ }
642
+ export async function clearMatchingCodexAuthFailure(agentHome, fingerprint, lease) {
643
+ const cleared = await withStateLock(agentHome, fingerprint, async () => {
644
+ const current = await readState(agentHome, fingerprint);
645
+ if (lease) {
646
+ if (!matchesProbe(current, lease)) {
647
+ forgetActiveProbe(agentHome, fingerprint, lease.probeId);
648
+ return false;
649
+ }
650
+ }
651
+ else if (current?.state !== "failed" || current.fingerprint !== fingerprint) {
652
+ return false;
653
+ }
654
+ const target = statePath(agentHome, fingerprint);
655
+ await fs.unlink(target).catch(() => undefined);
656
+ await fs.rmdir(path.dirname(target)).catch(() => undefined);
657
+ forgetActiveProbe(agentHome, fingerprint, lease?.probeId);
658
+ return true;
659
+ });
660
+ if (cleared !== null) {
661
+ await fs.rmdir(path.dirname(statePath(agentHome, fingerprint)))
662
+ .catch(() => undefined);
663
+ }
664
+ return cleared ?? false;
90
665
  }
91
666
  //# sourceMappingURL=readiness-gate.js.map