Yeachan-Heo/oh-my-codex · error · Error
[native-assets] publication-lock-timeout: ${path}; elapsed=$
Error message
[native-assets] publication-lock-timeout: ${path}; elapsed=${Math.round(performance.now() - started)}ms deadline=${lockWaitMs(env)}ms; ${diagnostic.classification}${owner}. Confirm no OMX hydration process is active for this cache key, remove only this named lock manually, then retry. What it means
The publication lock for a cache destination could not be acquired within the wait deadline (another holder kept it, creation raced with EEXIST until the timeout elapsed). The message includes the lock path, elapsed time, deadline, an inspected classification, and owner info, plus remediation: verify no OMX hydration is active for that key, remove only that named lock, retry.
Source
Thrown at src/cli/native-assets.ts:591
try {
const { bytesWritten } = await handle.write(recordBytes, 0, recordBytes.length, 0);
if (bytesWritten !== recordBytes.length) throw new Error('[native-assets] incomplete lock write');
const readback = Buffer.alloc(recordBytes.length);
const { bytesRead } = await handle.read(readback, 0, readback.length, 0);
if (bytesRead !== readback.length || !readback.equals(recordBytes)) throw new Error('[native-assets] lock readback mismatch');
const identity = await handle.stat();
const fileIdentity = { dev: identity.dev, ino: identity.ino, size: identity.size };
if (!identity.isFile() || identity.nlink !== 1) throw new Error('[native-assets] unsafe publication lock');
await reInspectPath(path, fileIdentity, true);
return { path, token, record, identity: fileIdentity };
} finally { await handle.close(); }
} catch (error) {
if (errno(error) !== 'EEXIST') throw error;
if (performance.now() >= deadline) {
const diagnostic = await inspectLock(path, binaryPath) ?? { path, classification: 'metadata-unavailable' as const };
const owner = diagnostic.owner ? ` owner=${JSON.stringify(diagnostic.owner)}` : '';
throw new Error(`[native-assets] publication-lock-timeout: ${path}; elapsed=${Math.round(performance.now() - started)}ms deadline=${lockWaitMs(env)}ms; ${diagnostic.classification}${owner}. Confirm no OMX hydration process is active for this cache key, remove only this named lock manually, then retry.`);
}
await new Promise<void>((done) => setTimeout(done, LOCK_RETRY_MS));
}
}
}
async function releaseCacheLock(lock: PublicationLock): Promise<ManagedNativeBinaryInspection | undefined> {
try {
const reopened = await readOpenedFile(lock.path, false, false);
if (!sameFile(lock.identity, reopened) || reopened.text !== lock.record) return { state: 'cleanup-failed' };
const current = await lstat(lock.path);
if (!sameFile(lock.identity, { dev: current.dev, ino: current.ino, size: current.size }) || !current.isFile() || current.nlink !== 1) return { state: 'cleanup-failed' };
await unlink(lock.path);
} catch (error) { return absent(error) ? undefined : { state: 'cleanup-failed' }; }
return undefined;
}
View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Confirm no other hydration process is running for that cache key, then delete exactly the lock path printed in the message and retry.
- Serialize installs (single postinstall) in CI to avoid lock contention.
- If it recurs, raise the lock wait via the library's env knob or pre-hydrate caches in the image build.
Example fix
# after confirming no hydration is active rm /path/from/error/message/.lock npm run your-hydrate-step
Defensive patterns
Strategy: retry
Try / catch
try { await hydrateNativeBinary(); } catch (e) {
const m = /publication-lock-timeout: ([^;]+)/.exec(String(e));
if (m) { /* confirm no active hydration, rm exactly m[1], retry once */ await rm(m[1], { force: true }); return hydrateNativeBinary(); }
throw e;
} Prevention
- Never run concurrent installs hydrating the same cache key
- Clean stale locks after SIGKILLed CI jobs
- Pre-hydrate caches in image builds to avoid runtime locking
When it happens
Trigger: acquireCacheLock when O_CREAT|O_EXCL returns EEXIST repeatedly past lockWaitMs — a concurrent hydration holding the lock, or a stale/crashed lock left behind after a SIGKILL or power loss.
Common situations: Parallel npm postinstall scripts hydrating the same cache key; a previously killed process leaving a stale lock; CI runners reusing a warm cache volume with leftover locks.
Understand the failure class
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- timed out waiting for tmux extended-keys lease lock: ${lockP
- [native-assets] incomplete lock write
- [native-assets] lock readback mismatch
- [native-assets] unsafe publication lock
- Failed to acquire AGENTS.md lock within timeout
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/e70ef5202f0fad57.
Report an issue: GitHub.