affaan-m/ECC · warning
Another Nasiko lifecycle operation won stale-lock recovery
Error message
Another Nasiko lifecycle operation won stale-lock recovery: ${lockPath}. What it means
After successfully removing a stale lock, the library re-attempts createLifecycleLock. If that create fails with EEXIST, another process created a new lock in the window between stale-lock removal and re-creation; it throws this error instead of looping. This is a benign lost-race indication.
Solutions
- Retry the operation after a short delay — the winner will finish and release the lock.
- Add mutual exclusion above the library (per-host flock or a CI concurrency group) to avoid recovery races.
- Confirm the winner is progressing (check the new lock's pid) and wait rather than fighting for the lock.
Example fix
// before
installNasiko({ yes: true }) // both recovery racers start
// after (CI)
concurrency: { group: 'nasiko-install', cancel-in-progress: false } # YAML: serialize jobs
installNasiko({ yes: true }) Defensive patterns
Strategy: retry
Validate before calling
// Check lock liveness before recovery to avoid racing other recoverers:
const lock = path.join(installDir, '.ecc-nasiko-lifecycle.lock')
const meta = JSON.parse(fs.readFileSync(lock, 'utf8'))
let alive = false
try { process.kill(meta.pid, 0); alive = true } catch (e) { alive = e.code !== 'ESRCH' }
if (alive) throw new Error('owner still running; no recovery needed') Try / catch
try {
await installNasiko({ yes: true })
} catch (e) {
if (e.message.includes('won stale-lock recovery')) {
await new Promise(r => setTimeout(r, 2000))
return installNasiko({ yes: true }) // let the winner finish first
}
throw e
} Prevention
- Use a single recovery coordinator per host/installDir.
- Serialize installs with CI concurrency groups.
- Back off and retry; the winner holds a valid lock that will be released shortly.
When it happens
Trigger: installNasiko/uninstallNasiko stale-lock recovery where a competing process acquires the lock immediately after the stale owner is removed — e.g. two recovery attempts firing simultaneously on a dead-owner lock.
Common situations: Two CI jobs both detecting the same stale lock and racing to recover it; a scheduled install colliding with a manual recovery; shared network filesystems with delayed metadata propagation amplifying the race window.
Related errors
- Another Nasiko lifecycle operation won lock acquisition
- capsule.lock_lost
- Nasiko lifecycle lock changed during stale-owner recovery
- 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/e079c79e33101334.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/nasiko-release.js:373
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 };
if (options.dryRun) return plan;
if (!options.yes) throw new Error('Nasiko installation requires explicit --yes consent.');
fs.mkdirSync(installDirectory, { recursive: true, mode: 0o755 });
assertPrivateInstallDirectory(installDirectory);
const releaseLock = acquireLifecycleLock(installDirectory);
const metadataPath = metadataPathFor(destination);View on GitHub (pinned to 8321021c54)