Skip to content

ruflo doctor leaves an orphaned WAL sidecar that blocks all subsequent memory writes #3161

Description

@antgabrielcst

ruflo doctor leaves an orphaned WAL sidecar that blocks all subsequent memory writes

Package: ruflo@3.38.20 (@claude-flow/cli@3.38.20)
Repo: https://github.com/ruvnet/claude-flow — file an issue at https://github.com/ruvnet/claude-flow/issues
Related: #2735 (the WAL-sidecar safety gate), #3024 (native bridge disabled on Windows)

Summary

ruflo doctor opens the memory database with a native better-sqlite3 WAL
connection and exits without checkpointing, leaving a 0-byte memory.db-wal
and a memory.db-shm on disk. The #2735 safety gate treats the mere presence
of those sidecars as proof of a live native writer, so every later
ruflo memory store in that project is refused — permanently, until the
sidecars are removed by hand.

The gate's own docstring states the assumption it relies on:

sidecars ... removed only on the last connection's clean close

doctor violates that assumption. The sidecars are orphans, not evidence of a
live holder, so the heuristic misfires and the CLI reports a condition that is
not true.

Environment

ruflo 3.38.20
Node v24.20.0
npm 11.19.0
OS Windows 11, 10.0.26200, win32 x64
Install local (npm install ruflo@3.38.20); also reproduces via npx ruflo@latest

Reproduces with the background daemon stopped, so the daemon is not involved.

Reproduction

# 0. start from a checkpointed database (no sidecars)
rm -f .swarm/memory.db-wal .swarm/memory.db-shm

# 1. a write succeeds
ruflo memory store --namespace repro --key antes --value v
#    -> [OK] Data stored successfully

# 2. run doctor
ruflo doctor >/dev/null 2>&1
#    -> leaves wal=0 bytes, shm=32768 bytes

# 3. the identical write now fails
ruflo memory store --namespace repro --key depois --value v

Step 3 output:

[ERROR] memory database has an active native WAL connection (found -wal/-shm
sidecar files) — refusing an unsafe sql.js whole-image write. Retry once the
native writer completes, or restore the native better-sqlite3 bridge.
Bridge unavailable: AgentDB native bridge disabled on Windows after #3024;
set CLAUDE_FLOW_ENABLE_NATIVE_BRIDGE_ON_WINDOWS=1 to opt in

Deleting the two sidecars restores writes immediately. No other command in the
CLI surface leaves them behind: bisecting doctor, config list, status,
mcp status, daemon status, agent list, swarm status, session list and
hooks list against a cleaned database, only doctor does.

Evidence that no writer is actually live

A wal_checkpoint(TRUNCATE) against the same database, at the moment the write
is being refused, reports no contention and removes both sidecars:

{"busy":0,"log":0,"checkpointed":0}

busy: 0 is SQLite reporting that no other connection holds the WAL. The
0-byte -wal says the same thing: there are no pending frames to recover.

Root cause

node_modules/@claude-flow/cli/dist/src/memory/memory-initializer.js:78

function hasNativeWalSidecars(dbPath) {
    try {
        return fs.existsSync(`${dbPath}-wal`) || fs.existsSync(`${dbPath}-shm`);
    }
    catch {
        return true;
    }
}

The gate is presence-based, not liveness-based. It cannot distinguish a
live native holder from an orphan left by an unclean close, so any command that
leaks sidecars bricks the write path for the whole project directory.

Impact

  • memory store — and every feature built on it — fails for the rest of the
    project's life until a user manually deletes internal database files.
  • The error message misdirects: it blames a live writer and the missing Windows
    native bridge, so users chase CLAUDE_FLOW_ENABLE_NATIVE_BRIDGE_ON_WINDOWS=1,
    which does not address the cause. Whether that variable "works" only reflects
    whether sidecars happen to be present, making the failure look intermittent.
  • ruflo doctor, the command users are told to run when something is wrong, is
    what breaks it.

Suggested fixes

  1. Make doctor close cleanly. Run PRAGMA wal_checkpoint(TRUNCATE) and
    close every connection it opens. This alone fixes the reported symptom.
  2. Make the gate liveness-based. Probe with wal_checkpoint(TRUNCATE) and
    treat busy === 0 as "no live holder, safe to proceed" — the sidecars are
    also removed as a side effect. This keeps memory CRUD silently degrades to a sql.js whole-image rename-over that can corrupt WAL databases under concurrent native writers #2735's guarantee (a real holder
    still returns busy !== 0 and the write is still refused) while removing the
    false positive, with no platform-specific process scanning.
  3. Improve the message. When the -wal is 0 bytes and a checkpoint reports
    busy: 0, say the sidecars are stale and name the recovery, instead of
    pointing at a native writer that does not exist.

Workaround for users

Checkpoint and close before writing; this is safe because a real writer makes
busy !== 0 and nothing is removed:

const Database = require('better-sqlite3');
const db = new Database('.swarm/memory.db');
const r = db.pragma('wal_checkpoint(TRUNCATE)')[0];
db.close();
if (r.busy !== 0) throw new Error('a live writer holds the WAL — do not delete sidecars');

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions