Skip to content

fix(rvf): version-gate + pid-guard the corrupt-lock detector (retires on aqe 3.12.3) - #31

Merged
pacphi merged 1 commit into
mainfrom
fix/rvf-corrupt-lock-detector-version-gated
Jul 18, 2026
Merged

pacphi merged 1 commit into
mainfrom
fix/rvf-corrupt-lock-detector-version-gated

Conversation

@pacphi

@pacphi pacphi commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Why

Our RVF corrupt-lock detector (src/lib/rvf.mjs) flagged any .rvf.lock whose first four bytes are FLVR and then deleted the store beside it. Measured against @ruvector/rvf-node 0.1.8 — and confirmed across every RVF lock on the dev machine (12/12) — that signal is unsound:

magic size old verdict
every .rvf.lock (incl. next to healthy stores) FLVR 104 B "corrupt" → delete store
every .rvf store SFVR 162 B empty … 31 MB —

FLVR is the normal lock-record magic (the store's own magic is SFVR); 162 bytes is a valid empty store. So the detector was firing on "a lock exists" and could delete a healthy store — including brain.rvf, a dual-write target that can hold writes never flushed to memory.db (real data loss). This is the red herring aqe #564 calls out, independently reproduced here.

The real fix is upstream

aqe 3.12.3 (#564) makes exports atomic (tmp+rename) and self-heals genuinely-unusable stores on the next run — non-destructively, pid-guarded. The sound corruption signal is "open fails and create fails", which needs the native binding; the kit can't observe it from the filesystem. So the kit should defer to aqe, not second-guess it.

What this PR does

  • Auto-retire the corrupt-lock scan once installed aqe ≥ 3.12.3 (aqeSelfHealsRvf gate). A 3.12.3 prerelease sorts below the release and keeps the legacy scan — the safe direction. No release-day code change needed; it flips on version detection.
  • Interim pid-guard for aqe < 3.12.3: only a stale lock (dead owner pid, read as u32 LE at offset 4 per rvf-node 0.1.8) can trip the scan, so it can never quarantine a store a live process holds. This closes the live-peer hazard today, on 3.12.2 — not just after the upgrade.
  • Keep the oversized backstop (#495 runaway-append mode, ~277 GB) — a different, still-sound signal.
  • Status/heal wording drops "rebuilt from memory.db"; points at aqe 3.12.3 self-heal.

Verified on the installed 3.12.2: gate reads false → scan ACTIVE (pid-guarded); it flips to RETIRED at 3.12.3.

Scope note

The residual pre-3.12.3 false positive — a stale lock beside a healthy populated store — is pre-existing and moot on upgrade. Fully fixing it would require the native open+create probe (out of scope); the real fix is adopting 3.12.3. Tracked in #30, which also carries the release-adoption checklist (delete the now-dead branch once aqe ≥ 3.12.3 is in).

Tests

tests/kit/rvf.test.mjs — 12 hermetic tests (synthetic FLVR lock records + injected version/pid/cap; no aqe install, no binding, no network): version gate across 3.12.2 / 3.12.3 / 3.13.0 / prerelease / null; stale-lock flagged pre-fix; ignored post-retirement; live-peer never flagged; non-FLVR ignored; oversized survives retirement; missing dir safe. The retirement gate and live-peer guard both fail on the old logic, verified by reverting.

Gates: 112 mjs + 43 cjs tests pass, eslint 0, typecheck 0.

Closes part of #30.

… on aqe 3.12.3)

Our RVF corrupt-lock detector fired on any `.rvf.lock` whose first four
bytes are `FLVR`, then DELETED the store beside it. Measured against
@ruvector/rvf-node 0.1.8 and confirmed across every RVF lock on the dev
machine (12/12), that signal is unsound: `FLVR` is the NORMAL lock-record
magic (the store's own magic is `SFVR`), so every lock matches — healthy
or not — and a 162-byte store is a valid EMPTY store, not a truncated one.
The detector was effectively firing on "a lock exists" and could delete a
healthy store, including `brain.rvf`, a dual-write target holding writes
never flushed to memory.db (real data loss).

aqe PR #564 (shipping in 3.12.3) fixes the actual bug (#563) at the root:
exports become atomic (tmp+rename) and genuinely-unusable stores self-heal
on the next run, non-destructively and pid-guarded. The sound corruption
signal is "open fails AND create fails", which needs the native binding —
the kit can't observe it from the filesystem.

So:
- Retire the corrupt-lock scan automatically once installed aqe >= 3.12.3
  (aqeSelfHealsRvf gate; a 3.12.3 prerelease sorts below the release and
  keeps the legacy scan, the safe direction).
- Until then, guard the legacy scan with the same pid-liveness check aqe
  added: only a STALE lock (dead owner pid) can trip it, so it can never
  quarantine a store a live process is holding — closes that hazard today,
  on 3.12.2, not just after the upgrade.
- Keep the oversized backstop (#495 runaway-append mode) — a different,
  still-sound signal.
- Status/heal wording no longer claims "rebuilt from memory.db"; it points
  at aqe 3.12.3 self-heal.

The residual pre-3.12.3 false positive (a stale lock beside a healthy
POPULATED store) is pre-existing and moot on upgrade; the sound fix is
adopting 3.12.3, tracked separately.

12 hermetic tests (synthetic FLVR lock records + injected version/pid/cap;
no aqe install, no binding, no network). The retirement gate and live-peer
guard both fail on the old logic, verified by reverting.
@pacphi
pacphi merged commit 9a1878b into main Jul 18, 2026
11 checks passed
@pacphi
pacphi deleted the fix/rvf-corrupt-lock-detector-version-gated branch July 18, 2026 19:37
pacphi added a commit that referenced this pull request Jul 18, 2026
…ing, docs audit (#35)

* feat(sync): live progress tickers; close the statusline footer wipe; strip retired RVF lock scan

- output: withProgress elapsed-time ticker (TTY-only, injectable seam)
  wrapped around every slow managed heal — the ~512 MB brain KB refresh
  previously sat silent for 4m25s with the prompt frozen
- statusline: root cause of the recurring footer wipe identified —
  @claude-flow/cli's version-stamped helper auto-refresh pristine-copies
  statusline.cjs on the FIRST ruflo command after an upgrade (observed:
  daemon start + statusline.cjs + .helpers-version mtimes matching at
  12:20:35). fixStatusline now triggers that refresh first
  (refreshRufloHelpers, subprocess, alsoRefreshGlobal), then injects onto
  the freshly-stamped copy; helperStampStale lets status flag the armed
  wipe before it fires. Verified live: footer survives ruflo commands
- rvf: strip the version-gated legacy corrupt-lock branch (PR #31's
  remnant) — dead code now that sync keeps aqe >= 3.12.3 everywhere. Only
  the oversized backstop (#495 runaway append) remains; the FLVR/SFVR
  history stays in the module header as a guard against reintroducing
  bytes-on-disk corruption heuristics
- tests: helper-stamp (9), output-progress (5), rvf rewritten for the
  reduced contract (8) — 125 kit tests green

* fix: remediate 4-lens brutal-honesty review findings (PR #34 wave 1)

Adversarial review (Linus/security/Ramsay/Bach lenses, independent agents)
over the branch diff. Everything actionable, remediated with proof:

Ramsay (test quality):
- F1: the refresh-before-inject ordering test used a sentinel-only fake that
  passed BOTH orderings; the fake now models the real pristine-copy wipe.
  Mutation-verified: moving the refresh after inject turns the test RED
- F2: the ticker never ticked under test — mocked clock now drives the
  interval, minute rollover (1m01s), and real caller labels
- F3/L2: refresh child now exits nonzero on rejection AND on upstream's
  resolved {blocked} (signature gate) — true means ran-unblocked; hung-child
  timeout injectable and tested
- F4: v-prefixed stamp no longer arms a permanent false-stale (latent bug);
  empty/garbage stamps pinned stale-by-design (self-correcting)
- F5: cap boundary (=== excluded, +1 flagged), only-.rvf candidacy pinned

Linus (correctness):
- scanRvf per-entry stat guard: a store renamed mid-scan by the aqe daemon
  no longer crashes ak status/sync (dangling-symlink test pins the race)
- staleness-oracle divergence: rufloCliVersion gains a createRequire
  resolver fallback, and the sync statusline step now also runs on
  providers-only plans (any ruflo-invoking heal is a potential wiper)
- statusline step wrapped in withProgress; ticker stdout invariant
  documented; stale "corrupt/oversized" wording in x/verify fixed

Bach (claims):
- sync no longer claims victory when the helper stamp is still stale after
  the heal (silent upstream-API no-op now warns instead of false success)
- rvf.mjs header rationale corrected: the stripped branch was reachable
  (status/--no-upgrade/offline), removed because its false-positive
  data-loss risk outweighed its protection — not because it was dead code

Security: clean (empirical: argv-safe subprocess, no shell, upstream
Ed25519 gate preserved; rmSync does not follow symlinks). One LOW
disclosure (alsoRefreshGlobal touches ~/.claude/helpers) → PR body.

137 tests green (was 125 at review start).

* fix: remediate holistic full-source review findings (wave 2)

Second adversarial wave: 4 independent agents (correctness/security/
coverage/docs-vs-reality) over all 35 source files, templates, bin, docs.

Correctness (holistic-linus):
- nativeSlots regression metric counted agentdb DIRECTORIES, not native
  bindings — the native→WASM backslide it exists to catch was invisible,
  and a benign tree reshape faked an alarm. Now counts native bindings
  (incl. the aqe slot)
- provider scope resolved by cwd-only .git probe: any run from a repo
  SUBDIR silently wrote ENABLE_*/AQE_LLM_PROVIDER into machine-wide user
  settings (and undo, run at the root, couldn't find them) while the aqe
  router/dual-agent gates skipped. All three gates now share a bounded
  repo-root walk (paths.repoRoot), project files anchor at the root
- upsertBlock/stripBlock ran an orphaned BEGIN "to end-of-file" — one
  corrupt sentinel deleted everything below it in ~/.claude/CLAUDE.md.
  Missing END now means append-fresh/leave-untouched; syncBlocks takes a
  one-time .bak like every other writer
- brain plugin-version fallback sorted lexically (0.9.0 > 0.10.0) → semver

Security (holistic-security; 2 LOW, 4 INFO, rest cleared with evidence):
- daemon reap confirms the live pid's cmdline is a ruflo daemon before
  SIGTERM (pidfile pid-reuse guard; POSIX ps, unconfirmable → never kill)
- dashboard rejects non-loopback Host headers (DNS-rebinding read guard)
- improvement-eval execSync shell strings → execFileSync argv (the one
  violation of the exec.mjs binding rule); setup.mjs SQL now uses bound
  params; .mcp.json deleted only when mcpServers was its sole content

Docs-vs-reality (holistic-bach):
- --no-security was INERT: documented, persisted, read by nothing. Now
  gates the aidefence heal in setup, the status security row (info, no
  fix), and sync's security branch
- dead updateHost/hostDrift removed (drift rides versions.mjs driftReport)
- KB size claims corrected to ~2 GB (disk truth: 1.9 GB); bin verify list
  gains harvest; README uninstall row shows the real flags

Coverage (holistic-ramsay): daemons.mjs was the only zero-coverage module
that kills processes — now pinned (kill selection, dead-pid degradation,
pid-reuse guard incl. a live spawned "daemon start" child, sweep parsing).
Deferred with eyes open: mcp.mjs deny-rule tests, uninstall.mjs fixture
tests, heal.mjs untested tail, dashboard client-esc DOM test.

149 tests green (was 137).

* docs: full documentation audit — accuracy, dead references, GitHub alerts

Walked every .md in the repo (37 files; none exceeds 1500 lines, so no
decomposition was warranted; docs/archive/* audited as frozen history,
content untouched by policy).

Accuracy (claims corrected to disk/code truth):
- TROUBLESHOOTING: RVF row no longer teaches the debunked FLVR-corruption
  story — oversized-only guard, aqe >= 3.12.3 self-heal cited (aqe #563),
  historical note explains .corrupt-<pid> droppings; statusline row now
  names the real wiper (helper auto-refresh on the first ruflo command
  after an upgrade) and the refresh-then-inject heal; adds the
  --no-security row and the harvest verify suite
- KB size ~512 MB → ~2 GB everywhere (disk truth: 1.9 GB) — MAINTAINER,
  README, setup help, heal/ruvnet-brain comments
- ruflo-reference stamp 3.28.x/2026-07-14 → 3.32.x/2026-07-18; dead
  `install.sh` attributions in three templates → ak-era commands
- project CLAUDE.md Build & Test: stale single-file command → pnpm
  test / pnpm run check (the real gates)
- archive index: living-docs pointers referenced two files that do not
  exist (BACKGROUND.md, CONDITIONAL-BLOCKS.md — both are archived);
  three "survives in bin//shell/" cells updated to v4 paths

Readability/labeling: GitHub alert syntax ([!IMPORTANT]/[!TIP]/[!NOTE]/
[!WARNING]) on the load-bearing callouts in README, TROUBLESHOOTING,
PROVIDERS (incl. the new repo-root scope note). markdownlint clean.

Also: branch renamed feat/sync-progress-statusline-convergence →
feat/ux-hardening-docs-audit (PR #34 retargeted); drops the unused
cmpVersions import stranded by the wave-2 dead-code removal.

* fix(ci): pid-reuse guard killed the Windows test runner — make it real on Windows

isDaemonProcess() returned true unconditionally on win32 ("no cheap sync
probe"), so the pid-reuse guard test — which feeds reap() the test runner's
own live pid to prove non-daemons are never killed — SIGTERM'd the runner
itself on windows-latest, failing daemons.test.mjs at file level across the
whole Node matrix (macOS/Linux green).

Fix the cause, not the test: the guard now probes the cmdline on Windows
too, via the same Win32_Process CIM query the process sweep already trusts
(pid numeric-coerced into the filter; unconfirmable → never kill). The
guard test now runs on every platform; only the kill-true path stays
POSIX-only (CI spawn/CIM timing, not a behavior difference).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant