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

  1. Simply retry the operation — the race is transient and the next attempt will see consistent state.
  2. Stop external cleanup jobs from touching the install directory's lock file.
  3. Serialize lifecycle operations across the machines/jobs sharing the directory.
  4. 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

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


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)