Skip to content

[BUG] ENOENT from proper-lockfile realpath() after proactive stale lock cleanup #670

@arthurliang

Description

@arthurliang

Bug Description

After the proactive stale lock cleanup (introduced in PR #626 for issues #622/#623) clears a stale .memory-write.lock file, subsequent proper-lockfile calls fail with:

ENOENT: no such file or directory, lstat '/Users/arthurliang/.openclaw/memory/lancedb-pro/.memory-write.lock'

Root Cause

The proactive cleanup (store.ts lines 221-230) unlinks the stale lock file:

if (existsSync(lockPath)) {
  try {
    const stat = statSync(lockPath);
    const ageMs = Date.now() - stat.mtimeMs;
    const staleThresholdMs = 5 * 60 * 1000;
    if (ageMs > staleThresholdMs) {
      try { unlinkSync(lockPath); } catch {}
      console.warn(`[memory-lancedb-pro] cleared stale lock: ${lockPath} ageMs=${ageMs}`);
    }
  } catch {}
}

This runs before lockfile.lock():

const release = await lockfile.lock(lockPath, {
  retries: { retries: 10, factor: 2, minTimeout: 200, maxTimeout: 5000 },
  stale: 10000,
});

In proper-lockfile/lib/lockfile.js, the resolveCanonicalPath function calls options.fs.realpath(file) on the lock target path (line 22). The default for realpath is true in proper-lockfile v4. When the lock file was just deleted by proactive cleanup, realpath() throws ENOENT because the target file does not exist.

Relevant proper-lockfile code (lockfile.js lines 15-22):

function resolveCanonicalPath(file, options, callback) {
    if (!options.realpath) {
        return callback(null, path.resolve(file));
    }
    // Use realpath to resolve symlinks
    // It also resolves relative paths
    options.fs.realpath(file, callback);
}

Observed Timeline

Time Event
07:35:17 smart-extractor acquired lock, wrote memories, process terminated without releasing
08:35:43.181 Proactive cleanup detected stale lock (ageMs=3635657 ≈ 60 min), deleted .memory-write.lock
08:35:43.184 3ms later: proper-lockfile.realpath() on deleted file → ENOENT → smart-extractor failed

Environment

  • memory-lancedb-pro: 1.1.0-beta.10
  • OS: macOS 15.6 (arm64)
  • OpenClaw gateway (agent main, multi-session)

Proposed Fix

Two complementary fixes:

Fix 1: Pass realpath: false to proper-lockfile

Since the lock path is always an absolute path to a known location, symlink resolution is unnecessary:

const release = await lockfile.lock(lockPath, {
  realpath: false,
  retries: { retries: 10, factor: 2, minTimeout: 200, maxTimeout: 5000 },
  stale: 10000,
});

Fix 2: Recreate lock file after proactive cleanup

After unlinking the stale lock file, recreate it so proper-lockfile has a valid target:

if (ageMs > staleThresholdMs) {
  try { unlinkSync(lockPath); } catch {}
  // Recreate the target file so proper-lockfile can lock it
  try { writeFileSync(lockPath, "", { flag: "wx" }); } catch {}
  console.warn(`[memory-lancedb-pro] cleared stale lock: ${lockPath} ageMs=${ageMs}`);
}

Recommended: Both fixes together

Fix 1 prevents the ENOENT from proper-lockfile. Fix 2 ensures the lock target file exists even if other code paths depend on it. Together they provide defense in depth.

Reproduction Steps

  1. Start a session with memory-lancedb-pro
  2. Trigger smart-extractor to write memories and acquire the file lock
  3. Kill the process (or let session reset) without releasing the lock
  4. Wait >5 minutes (stale threshold)
  5. Trigger another smart-extractor run
  6. Observe: stale lock cleanup runs, deletes lock file, next lock attempt fails with ENOENT

Related Issues

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions