affaan-m/ECC · warning
Nasiko lifecycle lock changed during stale-owner recovery
Error message
Nasiko lifecycle lock changed during stale-owner recovery: ${lockPath}. What it means
removeLockIfOwned removes the stale lock only if its current file identity (dev/ino) still matches the stats captured during inspection. If the lock file was replaced or removed by someone else in the meantime, removal is refused and this error is thrown — the lock state changed under us during recovery and it is unsafe to proceed.
Solutions
- Simply retry the operation — the race is transient and the next attempt will see consistent state.
- Stop external cleanup jobs from touching the install directory's lock file.
- Serialize lifecycle operations across the machines/jobs sharing the directory.
- Verify no other recovery attempt is running (check for multiple concurrent invocations) before retrying.
Example fix
// before
installNasiko({ yes: true }) // throws once during a race
// after
let last
for (let i = 0; i < 3; i++) {
try { last = installNasiko({ yes: true }); break }
catch (e) { if (!/changed during/.test(e.message)) throw e; await sleep(500) }
} Defensive patterns
Strategy: retry
Validate before calling
// Ensure exclusive access before recovery:
if (fs.existsSync(path.join(installDir, '.ecc-nasiko-lifecycle.lock'))) {
// only one recovery attempt at a time — coordinate via an external mutex/flock
} Try / catch
try {
await installNasiko({ yes: true })
} catch (e) {
if (e.message.includes('changed during stale-owner recovery')) {
await new Promise(r => setTimeout(r, 1000))
return installNasiko({ yes: true }) // next attempt sees consistent state
}
throw e
} Prevention
- Exclude lock files from external cleanup jobs (tmpwatch/cron).
- Attempt stale-lock recovery from one coordinator, not multiple jobs at once.
- Retry once on this error before surfacing it to users.
When it happens
Trigger: installNasiko/uninstallNasiko recovering a stale lock while a third process concurrently deletes and recreates the lock file between inspectLifecycleLock and removeLockIfOwned (inode mismatch).
Common situations: High-contention shared install directories in CI matrices; recovery racing with another recovery attempt; external cleanup scripts (tmpwatch/cron) removing lock files mid-operation.
Related errors
- Another Nasiko lifecycle operation won lock acquisition
- Another Nasiko lifecycle operation won stale-lock recovery
- capsule.lock_lost
- Another ECC process is updating Claude settings
- Another Nasiko lifecycle operation is already in progress…
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/92b52a8185752730.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/nasiko-release.js:367
try { existing = inspectLifecycleLock(lockPath, fileSystem); }
catch (error) {
if (error.code === 'ENOENT') {
try { return createLifecycleLock(lockPath, fileSystem); }
catch (retryError) {
if (retryError.code === 'EEXIST') {
throw new Error(`Another Nasiko lifecycle operation won lock acquisition: ${lockPath}.`);
}
throw retryError;
}
}
throw error;
}
const isProcessAlive = options.isProcessAlive || processIsAlive;
if (!existing || isProcessAlive(existing.metadata.pid)) {
throw new Error(`Another Nasiko lifecycle operation is already in progress; inspect ${lockPath} before recovering a stale lock.`);
}
if (!removeLockIfOwned(lockPath, existing.stats, fileSystem)) {
throw new Error(`Nasiko lifecycle lock changed during stale-owner recovery: ${lockPath}.`);
}
try {
return createLifecycleLock(lockPath, fileSystem);
} catch (error) {
if (error.code === 'EEXIST') {
throw new Error(`Another Nasiko lifecycle operation won stale-lock recovery: ${lockPath}.`);
}
throw error;
}
}
async function installNasiko(options = {}, dependencies = {}) {
const version = options.version || 'v0.1.0';
const base = getQualifiedRelease(version, dependencies.platform || process.platform, dependencies.arch || process.arch);
const release = dependencies.releaseOverride ? { ...base, ...dependencies.releaseOverride } : base;
const installDirectory = validateInstallDirectory(options.installDir || defaultInstallDirectory(release, dependencies.environment || process.env, dependencies.homeDirectory || os.homedir()));
const destination = path.join(installDirectory, release.binaryName);
const plan = { dryRun: Boolean(options.dryRun), version, platform: release.os, architecture: release.arch, manifestDigest: release.manifestDigest, binaryDigest: release.binaryDigest, registryOrigin: REGISTRY_ORIGIN, destination, license: release.license, sourceUrl: release.sourceUrl };View on GitHub (pinned to 8321021c54)