Skip to content

feat(memory-health): fix Onboarding Console scoring the wrong project - #1

Open
Tigon32 wants to merge 1 commit into
sparkling:mainfrom
Tigon32:fix/memory-health-kickrefresh-cwd
Open

feat(memory-health): fix Onboarding Console scoring the wrong project#1
Tigon32 wants to merge 1 commit into
sparkling:mainfrom
Tigon32:fix/memory-health-kickrefresh-cwd

Conversation

@Tigon32

@Tigon32 Tigon32 commented Jul 24, 2026

Copy link
Copy Markdown

The bug

ruvnet-brain's Onboarding Console (the "Your memory, proven" card, served via /brain-console)
is correct on the very first request — startServer({ cwd = process.cwd() }) captures the real
launch directory, and gatherMemory(cwd) checks that project's .swarm/memory.db. But every read
after that is served from an on-disk cache, refreshed by a detached child process that
kickRefresh() spawns at most once per 15 seconds:

const child = spawn(process.execPath, [SELF, '--refresh-cache'],
  { detached: true, stdio: 'ignore', cwd: REPO });

REPO (path.dirname(__dirname), the plugin's own install directory) is hardcoded as the child's
cwd. The --refresh-cache branch calls gatherState(process.cwd()), which resolves to REPO
because that's the cwd the child was spawned with — never the project actually being served.
REPO has no project .swarm/memory.db of its own, so the health check genuinely fails, for the
wrong project, and that wrong result is written straight into the on-disk cache and served to
every subsequent request, including the very next page load, re-derived every 15s by the next kick.

Measured live (2026-07-24): a project with a genuine, 122MB, actively-written .swarm/memory.db
(separately confirmed healthy via the AgentDB/ruflo memory bridge: 10,875 entries, 16,390 patterns
learned) scored 33/100, with "Liveness: fail — this project has no memory store (.swarm/memory.db) yet" — because the number being shown was ruvnet-brain's own plugin directory,
never the project's.

The fix

Read the live process.cwd() at spawn time instead of the hardcoded REPO constant:

const child = spawn(process.execPath, [SELF, '--refresh-cache'],
  { detached: true, stdio: 'ignore', cwd: process.cwd() });

This is sound because the console's server process never calls process.chdir() anywhere in the
file (grepped, zero hits) — so process.cwd() read at the exact moment kickRefresh() fires is
always identical to the cwd the server was launched with, the same value startServer()'s own
default parameter and the --serve/--print-state CLI branches already use. No argument threading
needed through kickRefresh()'s signature or either of its two call sites.

Single literal edit — the file's other 8 uses of REPO (CONSOLE_DIR, the SBOM path, the
script-runner's own legitimate cwd: REPO, gatesSurvey, etc.) are untouched, verified
byte-for-byte in the test.

What's in this PR

  • lib/memory-health/patcher.mjs + commands.mjs + README.md — the new patch target, following
    the exact shape of the existing verify-interface/design-wall targets
  • docs/adr/ADR-025-memory-health-kickrefresh-inherits-server-cwd.md — full context/decision/consequences
  • Wired into lib/plugin-registry.mjs, lib/plugin-compose.mjs, bin/cli.mjs, root README.md, AGENTS.md
  • test/memory-health.mjs — behavioural test against the real vendor file (via its .rsp-backup if
    installed, else skips if ruvnet-brain isn't present): proves the buggy anchor is unique, the fix
    touches ONLY the intended spawn call (all 8 other REPO usages verified unchanged), apply is
    idempotent, and uninstall restores byte-identical vendor
  • test/fixtures.mjs — added the memoryHealth recognizer to looksPatched (needed so
    pristineBytes() can tell a genuinely-patched file from a pristine one for this target)
  • scripts/run-tests.sh — added memory-health to SUITES

Verification

  • node test/memory-health.mjs — all 8 checks pass
  • Full suite (scripts/run-tests.sh) — no new failures introduced. Two pre-existing failures
    (reporting.mjs R4 — a memory patch-target precondition not installed on my machine;
    design-wall.mjs DW1/DW4) reproduce identically on main at the same commit, with and without
    this patch — confirmed via a clean worktree
  • node scripts/md-lint.mjs — 0 problems across 44 files
  • Applied live on my machine against the real
    ~/.claude/plugins/marketplaces/ruvnet-brain/scripts/onboarding-console.mjs, restarted the
    console server from a real project directory, and confirmed the on-disk cache written by the
    background refresh child now reports that project (score 67, liveness: ok) instead of the
    plugin's own directory (score 33, liveness: fail)

Upstream

Filed as ruvnet-brain (memory-health card) alongside this patch, same convention as
verify-interface/design-wall's existing upstream issue links.

ruvnet-brain's Onboarding Console ("Your memory, proven" card) is correct on
the very first request, but every subsequent read is served from an on-disk
cache refreshed by a detached child process. kickRefresh() spawns that child
with `cwd: REPO` -- the plugin's own install directory -- instead of the
server's actual launch directory, so the refresh always scores the PLUGIN's
own (memory-store-less) directory and writes that wrong result into the
cache served to every subsequent request.

Measured live (2026-07-24): a project with a genuine, 122MB, actively-written
.swarm/memory.db scored 33/100, "Liveness: fail -- this project has no
memory store yet", because the number shown was ruvnet-brain's own
directory, never the project's.

The console's server process never calls process.chdir() anywhere in the
file (grepped, zero hits), so process.cwd() read at the moment
kickRefresh() fires is always identical to the cwd the server was launched
with -- the same value startServer()'s own default parameter and the
--serve/--print-state CLI branches already use. No argument threading
needed through kickRefresh()'s signature or either of its two call sites.

Adds the `memory-health` patch target (patcher, commands, README, ADR-025),
wires it into plugin-registry/plugin-compose/CLI/docs the same way
verify-interface and design-wall are wired, and adds a behavioural test
(test/memory-health.mjs) that proves: the buggy anchor is unique in the real
vendor file, the fix touches ONLY the intended spawn call (the file's other
8 REPO usages are verified byte-identical before and after), apply is
idempotent, and uninstall restores byte-identical vendor.

Verified end-to-end on a live install: patched the real
~/.claude/plugins/marketplaces/ruvnet-brain/scripts/onboarding-console.mjs,
restarted the console server from a real project directory, and confirmed
the on-disk cache written by the background refresh child now reports that
project (score 67, liveness: ok) instead of the plugin's own directory
(score 33, liveness: fail).

Full test suite: 0 new failures. Two suites (reporting.mjs R4, an
unrelated missing `memory` patch-target precondition on this machine; and
design-wall.mjs DW1/DW4) fail identically on main before this change --
confirmed via a clean worktree at the same commit, both with and without
this patch applied.
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