Skip to content

Add an experimental Codex session supervisor#146

Closed
ndycode wants to merge 1 commit intomainfrom
git-clean/05-session-supervisor
Closed

Add an experimental Codex session supervisor#146
ndycode wants to merge 1 commit intomainfrom
git-clean/05-session-supervisor

Conversation

@ndycode
Copy link
Owner

@ndycode ndycode commented Mar 20, 2026

Summary

Adds an experimental wrapper that can resume interactive Codex sessions after account rotation instead of forcing a cold restart.

What changed

  • add scripts/codex-supervisor.js and wire it through the Codex wrapper
  • expose codexCliSessionSupervisor behind an opt-in setting
  • add focused supervisor, wrapper, settings, and quota-probe coverage
  • keep the preemptive 5h threshold at 5 outside the supervisor split
  • harden lock cleanup, env fallback accessors, and snapshot cache key redaction

Validation

  • npm run lint
  • npm run typecheck
  • npm run test:session-supervisor:smoke

Supersedes part of #139.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr introduces an experimental codex session supervisor — a 2251-line wrapper (scripts/codex-supervisor.js) that monitors running interactive codex sessions, detects quota pressure or rate-limit events, pre-warms a successor account, gracefully terminates the child, and relaunches with codex resume <session-id>, all without forcing a cold restart. the feature is gated behind codexCliSessionSupervisor: false by default and exposed as a toggle in the experimental settings ui.

key findings:

  • parseNumberEnv 4th-arg bug: createRuntimeConfigAccessors calls parseNumberEnv(..., 0, 100) for both quota-percent fallback accessors, but parseNumberEnv only accepts 3 parameters — the max=100 cap is silently dropped. in the fallback path (no compiled dist), an env var value greater than 100 produces a threshold that always triggers rotation on every monitor poll cycle.
  • TOCTOU race in stale-lock eviction: withSupervisorStorageLock checks staleness and unlinks the lock file in two separate steps. on windows with concurrent supervisor processes, the safeUnlink branch (for legacy locks without ownerId) can delete a live lock acquired by a peer process between the stat and the unlink calls.
  • windows / token safety: spawnRealCodex passes process.env directly to the child, inheriting all tokens — intentional for a cli wrapper but undocumented. the safeUnlink retry loop correctly handles transient windows ntfs errors.
  • test coverage gap: no vitest case asserts that quota-percent env vars above 100 are clamped, leaving the parseNumberEnv max-arg bug undetectable by the current test suite.
  • the abort-signal plumbing in lib/quota-probe.ts, the session binding file scanner, the pre-warm selection pipeline, and the hotkey remapping in settings-hub.ts are all well-implemented.

Confidence Score: 3/5

  • not safe to merge as-is — the silent parseNumberEnv max-arg drop can cause runaway rotation when the fallback config path is active
  • two logic issues lower the score: the unbounded quota-percent threshold in the fallback accessor path, and the TOCTOU race in stale-lock eviction. the rest of the implementation is solid with good abort-signal hygiene and windows-aware retry loops. feature is opt-in so blast radius is limited, but the rotation loop bug could silently exhaust all accounts on a misconfigured machine.
  • scripts/codex-supervisor.js — createRuntimeConfigAccessors (parseNumberEnv 4th arg), withSupervisorStorageLock (stale-lock TOCTOU)

Important Files Changed

Filename Overview
scripts/codex-supervisor.js New 2251-line supervisor implementing session monitoring, account pre-warming, and restart coordination. Two logic issues: parseNumberEnv called with a 4th max=100 arg it silently ignores (fallback quota thresholds unbounded), and a TOCTOU race in stale-lock eviction relevant on Windows.
scripts/codex.js Wires the supervisor into the main entry point via runCodexSupervisorIfEnabled. The supervisorDidForward flag correctly prevents double auto-sync. forwardToRealCodexWithStartupSync is called on each supervisor restart cycle, which is intentional but undocumented.
scripts/codex-routing.js Adds findPrimaryCodexCommand, hasTopLevelHelpOrVersionFlag, and splitCodexCommandArgs helpers. Logic correctly handles --, flag-value pairs, and --config= inline forms. No issues found.
lib/quota-probe.ts Adds AbortSignal support to fetchCodexQuotaSnapshot. Signal is wired to the inner AbortController via event listener with proper cleanup in finally. The abort-before-loop-entry check is correct. No issues found.
lib/config.ts Adds getCodexCliSessionSupervisor accessor using the existing resolveBooleanSetting pattern. Defaults to false (opt-in). No issues found.
test/codex-supervisor.test.ts 1801-line test suite covering session binding, lock mechanics, quota pressure, abort signal propagation, and snapshot cache key redaction. No test covers quota env var > 100, which would expose the parseNumberEnv max-arg bug. Missing vitest coverage for the TOCTOU stale-lock race under concurrent processes.
test/codex-bin-wrapper.test.ts Extends the bin-wrapper integration suite with supervisor fixture helpers and five new test cases covering supervisor-forwarded commands, double-sync prevention, abort-sentinel behaviour, and interactive command routing. Solid coverage.

Sequence Diagram

sequenceDiagram
    participant W as codex.js (wrapper)
    participant S as codex-supervisor.js
    participant L as Storage Lock (.supervisor.lock)
    participant A as AccountManager
    participant Q as quota-probe
    participant C as codex (child process)

    W->>S: runCodexSupervisorIfEnabled({ codexBin, rawArgs })
    S->>L: withSupervisorStorageLock → loadFromDisk
    L-->>A: AccountManager.loadFromDisk()
    A-->>S: manager + currentAccount
    S->>Q: ensureLaunchableAccount (probe batch)
    Q-->>S: ready account selected + persisted

    S->>C: spawnRealCodex(codexBin, initialArgs)
    activate C

    loop monitor loop (every pollMs)
        S->>S: waitForSessionBinding (scan .jsonl files)
        S->>L: withSupervisorStorageLock → loadCurrentState
        L-->>A: AccountManager.loadFromDisk()
        S->>Q: probeAccountSnapshot(currentAccount)
        Q-->>S: quota snapshot

        alt quota near threshold (prewarm)
            S->>Q: maybeStartPreparedResumeSelection (background)
            Q-->>S: pre-warmed successor account
        end

        alt quota exhausted / rate-limited (rotate)
            S->>C: requestChildRestart (SIGINT → SIGTERM → SIGKILL)
            deactivate C
            S->>L: markCurrentAccountForRestart
            S->>Q: commitPreparedSelection or ensureLaunchableAccount
            Q-->>S: next account committed
            S->>C: spawnRealCodex(codexBin, resume sessionId)
            activate C
        end
    end

    C-->>S: exit (code, signal)
    deactivate C
    S-->>W: supervisedExitCode
    W->>W: autoSyncManagerActiveSelectionIfEnabled (if needed)
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: scripts/codex-supervisor.js
Line: 621-637

Comment:
**`parseNumberEnv` ignores 4th `max` argument — quota thresholds unbounded in fallback path**

`parseNumberEnv` is defined as `function parseNumberEnv(name, fallback, min = 0)` — only 3 parameters. the `100` passed as the 4th argument here (and for `7d` below) is silently dropped.

this means, when the compiled dist isn't present and someone sets `CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT=200` (or any value > 100), the threshold is returned as-is. since `remaining5h` can't exceed 100, `remaining5h <= threshold5h` becomes `100 <= 200 = true` — the supervisor would **always** trigger rotation on every poll cycle, even for healthy accounts. this is a real regression risk on misconfigured machines.

fix: either add a `max` param to `parseNumberEnv` or clamp in-line:

```js
getPreemptiveQuotaRemainingPercent5h:
    configModule?.getPreemptiveQuotaRemainingPercent5h ??
    ((pluginConfig) =>
        Math.min(
            100,
            parseNumberEnv(
                "CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT",
                pluginConfig?.preemptiveQuotaRemainingPercent5h ?? 5,
                0,
            ),
        )),
getPreemptiveQuotaRemainingPercent7d:
    configModule?.getPreemptiveQuotaRemainingPercent7d ??
    ((pluginConfig) =>
        Math.min(
            100,
            parseNumberEnv(
                "CODEX_AUTH_PREEMPTIVE_QUOTA_7D_REMAINING_PCT",
                pluginConfig?.preemptiveQuotaRemainingPercent7d ?? 5,
                0,
            ),
        )),
```

no existing test exercises env values > 100 — the bug is invisible to the current suite.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/codex-supervisor.js
Line: 2072-2077

Comment:
**Token safety: child inherits full `process.env` including all `CODEX_AUTH_*` secrets**

`spawnRealCodex` passes `process.env` directly to the child. this is intentional for cli forwarding, but means the codex child process inherits every credential: `CODEX_AUTH_CLI_SESSION_SUPERVISOR`, access tokens, and any refresh tokens that may be slotted into env by upstream wiring. worth a comment noting this is deliberate — otherwise it looks like an accidental secret leak to future reviewers.

on windows, `PATH` injection via env is also a risk if `process.env.PATH` is ever tampered with before reaching this call. consider documenting the explicit trust boundary here.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/codex-supervisor.js
Line: 964-1081

Comment:
**Concurrency: TOCTOU window in stale-lock eviction on Windows**

the `isSupervisorLockStale``readSupervisorLockPayload``safeUnlink` / `safeUnlinkOwnedSupervisorLock` sequence is not atomic. if two supervisor processes both see the lock as stale at the same instant:

1. process A reads stale payload (owner = "old"), calls `safeUnlinkOwnedSupervisorLock("old")` → succeeds
2. process A acquires the lock and writes a new payload (owner = "A")
3. process B then calls `safeUnlink(lockPath)` on a lock it no longer owns — removing process A's live lock

the `safeUnlinkOwnedSupervisorLock` check mitigates most cases, but the raw `safeUnlink(lockPath)` branch (for payloads lacking `ownerId`) has no owner check and is vulnerable to this race. on windows, this is more likely due to slower fs operations and wider stale windows.

worst case: a live lock is deleted mid-operation, two processes hold the lock simultaneously, and account state is corrupted. while low-probability, consider using a write-verify cycle (read → compare owner → conditional unlink) or an atomic rename to replace stale locks.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/codex-supervisor.js
Line: 375-379

Comment:
**Module-level singleton maps survive process-level test isolation but not `--pool=forks`**

`snapshotProbeCache` and `sessionRolloutPathById` are module-level singletons. the `afterEach` cleanup in `codex-supervisor.test.ts` calls `clearAllProbeSnapshotCache` and `clearSessionBindingPathCache`, which is correct for sequential tests.

however, if vitest is ever run with `--pool=vmThreads` (where modules are shared across workers), state will bleed between test files. the `--testOnly` export + env-gated exposure pattern is sound, but worth noting: any future test that imports the module in a shared vm context will see contaminated cache state if the `afterEach` hasn't fired.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/codex.js
Line: 2613-2634

Comment:
**`autoSyncManagerActiveSelectionIfEnabled` called inside every supervisor restart cycle**

`forwardToRealCodexWithStartupSync` is used as the `forwardToRealCodex` callback for the supervisor, and the supervisor calls it for each account-rotation resume. this means `autoSyncManagerActiveSelectionIfEnabled()` fires on every restart, not just the first launch — which is intentional (sync the new account), but could cause observable delays or duplicate sync writes if the rotation fires quickly in succession.

no issue in the happy path, but worth a comment so the next reviewer doesn't optimise it away thinking it's an accidental repeat.

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "Add experimental Cod..."

Greptile also left 2 inline comments on this PR.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Warning

Rate limit exceeded

@ndycode has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 18 minutes and 39 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2cba597b-eb33-46b8-a772-0dfeeede186b

📥 Commits

Reviewing files that changed from the base of the PR and between 1be5e95 and b086c75.

📒 Files selected for processing (14)
  • lib/codex-manager/settings-hub.ts
  • lib/config.ts
  • lib/quota-probe.ts
  • lib/schemas.ts
  • lib/ui/copy.ts
  • package.json
  • scripts/codex-routing.js
  • scripts/codex-supervisor.js
  • scripts/codex.js
  • test/codex-bin-wrapper.test.ts
  • test/codex-supervisor.test.ts
  • test/plugin-config.test.ts
  • test/quota-probe.test.ts
  • test/settings-hub-utils.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch git-clean/05-session-supervisor
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch git-clean/05-session-supervisor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ndycode
Copy link
Owner Author

ndycode commented Mar 20, 2026

@coderabbitai review
@greptileai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

@ndycode: on it — triggering a full review of the pr now.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@ndycode ndycode closed this Mar 20, 2026
@ndycode ndycode deleted the git-clean/05-session-supervisor branch March 20, 2026 19:01
Comment on lines +621 to +637
}
console.warn(
`codex-multi-auth: treating unreadable supervisor lock as stale at ${lockPath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
return true;
}
}

async function withSupervisorStorageLock(runtime, fn, signal) {
const lockPath = getSupervisorStorageLockPath(runtime);
const lockDir = dirname(lockPath);
const waitMs = parseNumberEnv(
"CODEX_AUTH_CLI_SESSION_LOCK_WAIT_MS",
DEFAULT_STORAGE_LOCK_WAIT_MS,
0,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 parseNumberEnv ignores 4th max argument — quota thresholds unbounded in fallback path

parseNumberEnv is defined as function parseNumberEnv(name, fallback, min = 0) — only 3 parameters. the 100 passed as the 4th argument here (and for 7d below) is silently dropped.

this means, when the compiled dist isn't present and someone sets CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT=200 (or any value > 100), the threshold is returned as-is. since remaining5h can't exceed 100, remaining5h <= threshold5h becomes 100 <= 200 = true — the supervisor would always trigger rotation on every poll cycle, even for healthy accounts. this is a real regression risk on misconfigured machines.

fix: either add a max param to parseNumberEnv or clamp in-line:

getPreemptiveQuotaRemainingPercent5h:
    configModule?.getPreemptiveQuotaRemainingPercent5h ??
    ((pluginConfig) =>
        Math.min(
            100,
            parseNumberEnv(
                "CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT",
                pluginConfig?.preemptiveQuotaRemainingPercent5h ?? 5,
                0,
            ),
        )),
getPreemptiveQuotaRemainingPercent7d:
    configModule?.getPreemptiveQuotaRemainingPercent7d ??
    ((pluginConfig) =>
        Math.min(
            100,
            parseNumberEnv(
                "CODEX_AUTH_PREEMPTIVE_QUOTA_7D_REMAINING_PCT",
                pluginConfig?.preemptiveQuotaRemainingPercent7d ?? 5,
                0,
            ),
        )),

no existing test exercises env values > 100 — the bug is invisible to the current suite.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/codex-supervisor.js
Line: 621-637

Comment:
**`parseNumberEnv` ignores 4th `max` argument — quota thresholds unbounded in fallback path**

`parseNumberEnv` is defined as `function parseNumberEnv(name, fallback, min = 0)` — only 3 parameters. the `100` passed as the 4th argument here (and for `7d` below) is silently dropped.

this means, when the compiled dist isn't present and someone sets `CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT=200` (or any value > 100), the threshold is returned as-is. since `remaining5h` can't exceed 100, `remaining5h <= threshold5h` becomes `100 <= 200 = true` — the supervisor would **always** trigger rotation on every poll cycle, even for healthy accounts. this is a real regression risk on misconfigured machines.

fix: either add a `max` param to `parseNumberEnv` or clamp in-line:

```js
getPreemptiveQuotaRemainingPercent5h:
    configModule?.getPreemptiveQuotaRemainingPercent5h ??
    ((pluginConfig) =>
        Math.min(
            100,
            parseNumberEnv(
                "CODEX_AUTH_PREEMPTIVE_QUOTA_5H_REMAINING_PCT",
                pluginConfig?.preemptiveQuotaRemainingPercent5h ?? 5,
                0,
            ),
        )),
getPreemptiveQuotaRemainingPercent7d:
    configModule?.getPreemptiveQuotaRemainingPercent7d ??
    ((pluginConfig) =>
        Math.min(
            100,
            parseNumberEnv(
                "CODEX_AUTH_PREEMPTIVE_QUOTA_7D_REMAINING_PCT",
                pluginConfig?.preemptiveQuotaRemainingPercent7d ?? 5,
                0,
            ),
        )),
```

no existing test exercises env values > 100 — the bug is invisible to the current suite.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Comment on lines +964 to +1081
);
const near5h =
typeof remaining5h === "number" && remaining5h <= threshold5h;
const near7d =
typeof remaining7d === "number" && remaining7d <= threshold7d;
const prewarm5h =
typeof remaining5h === "number" && remaining5h <= prewarmThreshold5h;
const prewarm7d =
typeof remaining7d === "number" && remaining7d <= prewarmThreshold7d;

if (!near5h && !near7d) {
return {
prewarm: prewarm5h || prewarm7d,
rotate: false,
reason: "none",
waitMs: 0,
remaining5h,
remaining7d,
};
}

return {
prewarm: true,
rotate: true,
reason: "quota-near-exhaustion",
waitMs: computeWaitMsFromSnapshot(snapshot),
remaining5h,
remaining7d,
};
}

function evaluateQuotaSnapshot(snapshot, runtime, pluginConfig) {
const pressure = computeQuotaPressure(snapshot, runtime, pluginConfig);
return {
rotate: pressure.rotate,
reason: pressure.reason,
waitMs: pressure.waitMs,
};
}

function getSnapshotCacheKey(account) {
if (!account) return "";
const segments = [
`${account.accountId ?? ""}`,
`${account.email ?? ""}`,
`${account.index ?? ""}`,
];
return segments.some((segment) => segment.length > 0) ? segments.join("|") : "";
}

function getSnapshotCacheTtlMs() {
return parseNumberEnv(
"CODEX_AUTH_CLI_SESSION_SNAPSHOT_CACHE_TTL_MS",
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
0,
);
}

function clearProbeSnapshotCache(account) {
const cacheKey = getSnapshotCacheKey(account);
if (!cacheKey) return;
snapshotProbeCache.delete(cacheKey);
}

function clearAllProbeSnapshotCache() {
snapshotProbeCache.clear();
}

function readCachedProbeSnapshot(account) {
const cacheKey = getSnapshotCacheKey(account);
if (!cacheKey) return null;
const entry = snapshotProbeCache.get(cacheKey);
if (!entry?.snapshot || entry.expiresAt <= Date.now()) {
if (entry && !entry.pending) {
snapshotProbeCache.delete(cacheKey);
}
return null;
}
return entry.snapshot;
}

function rememberProbeSnapshot(account, snapshot) {
const cacheKey = getSnapshotCacheKey(account);
if (!cacheKey) return;
const ttlMs = getSnapshotCacheTtlMs();
if (ttlMs <= 0) {
snapshotProbeCache.delete(cacheKey);
return;
}
const current = snapshotProbeCache.get(cacheKey);
snapshotProbeCache.set(cacheKey, {
...current,
snapshot,
expiresAt: Date.now() + ttlMs,
});
}

async function probeAccountSnapshot(runtime, account, signal, timeoutMs, options = {}) {
if (signal?.aborted) {
throw createAbortError("Quota probe aborted");
}
if (!account?.accountId || !account?.access) {
return null;
}
const cacheKey = getSnapshotCacheKey(account);
let pendingResolver = null;
let pendingRejecter = null;
let pendingPromise = null;
if (options.useCache !== false) {
const cachedSnapshot = readCachedProbeSnapshot(account);
if (cachedSnapshot) {
return cachedSnapshot;
}
const pendingEntry = cacheKey ? snapshotProbeCache.get(cacheKey) : null;
if (pendingEntry?.pending) {
return abortablePromise(
pendingEntry.pending,
signal,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Concurrency: TOCTOU window in stale-lock eviction on Windows

the isSupervisorLockStalereadSupervisorLockPayloadsafeUnlink / safeUnlinkOwnedSupervisorLock sequence is not atomic. if two supervisor processes both see the lock as stale at the same instant:

  1. process A reads stale payload (owner = "old"), calls safeUnlinkOwnedSupervisorLock("old") → succeeds
  2. process A acquires the lock and writes a new payload (owner = "A")
  3. process B then calls safeUnlink(lockPath) on a lock it no longer owns — removing process A's live lock

the safeUnlinkOwnedSupervisorLock check mitigates most cases, but the raw safeUnlink(lockPath) branch (for payloads lacking ownerId) has no owner check and is vulnerable to this race. on windows, this is more likely due to slower fs operations and wider stale windows.

worst case: a live lock is deleted mid-operation, two processes hold the lock simultaneously, and account state is corrupted. while low-probability, consider using a write-verify cycle (read → compare owner → conditional unlink) or an atomic rename to replace stale locks.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/codex-supervisor.js
Line: 964-1081

Comment:
**Concurrency: TOCTOU window in stale-lock eviction on Windows**

the `isSupervisorLockStale``readSupervisorLockPayload``safeUnlink` / `safeUnlinkOwnedSupervisorLock` sequence is not atomic. if two supervisor processes both see the lock as stale at the same instant:

1. process A reads stale payload (owner = "old"), calls `safeUnlinkOwnedSupervisorLock("old")` → succeeds
2. process A acquires the lock and writes a new payload (owner = "A")
3. process B then calls `safeUnlink(lockPath)` on a lock it no longer owns — removing process A's live lock

the `safeUnlinkOwnedSupervisorLock` check mitigates most cases, but the raw `safeUnlink(lockPath)` branch (for payloads lacking `ownerId`) has no owner check and is vulnerable to this race. on windows, this is more likely due to slower fs operations and wider stale windows.

worst case: a live lock is deleted mid-operation, two processes hold the lock simultaneously, and account state is corrupted. while low-probability, consider using a write-verify cycle (read → compare owner → conditional unlink) or an atomic rename to replace stale locks.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant