Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Rfdb Stale Node Cleanup

BSecurity

Recover from stale RFDB nodes that survive `commit_batch` cleanup despite having their `file` field listed in `changed_files`. Use when: (1) re-running an enricher/orchestrator step but old data with the same file field stays in the graph, (2) `commit_batch(changed_files=[X], ...)` reports success but a query later still returns nodes with `file = X`, (3) you re-pointed structural nodes (e.g., DIRECTORY/FILE) from a synthetic file path to real file paths and the old synthetic-path nodes won't...

36 stars
0 votes
0 copies
0 views
Added 9/20/2026
developmentpythonrustgobashnodeapibackend

Works with

claude codecliapi

Security Analysis

B88/100
criticalDownloads and executes remote scripts — classic supply chain attack

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add Disentinel/grafema --skill rfdb-stale-node-cleanup --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rfdb Stale Node Cleanup?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Rfdb Stale Node Cleanup
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/disentinel-rfdb-stale-node-cleanup/badge)](https://www.skillsdirectory.com/skills/disentinel-rfdb-stale-node-cleanup)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: rfdb-stale-node-cleanup
description: |
  Recover from stale RFDB nodes that survive `commit_batch` cleanup despite
  having their `file` field listed in `changed_files`. Use when:
  (1) re-running an enricher/orchestrator step but old data with the same file
  field stays in the graph, (2) `commit_batch(changed_files=[X], ...)` reports
  success but a query later still returns nodes with `file = X`, (3) you
  re-pointed structural nodes (e.g., DIRECTORY/FILE) from a synthetic file
  path to real file paths and the old synthetic-path nodes won't go away,
  (4) graph-stream / find_by_type returns a mix of "stale" and "new" nodes
  for the same logical entity. Root cause: `handle_commit_batch` calls
  `engine.find_by_attr({file: X})` to enumerate nodes-to-delete, but that
  query path returns only a PARTIAL set after compaction (L1 segments and
  the file→nodes index can desync). Workaround: enumerate stale nodes via
  the TS rfdb client `queryNodes({file: X})` (different code path that finds
  all of them) and call `deleteNode(id)` per node, then re-commit the
  intended state. Related: rfdb-manifest-l1-carryforward (same L1 family).
author: Claude Code
version: 1.0.0
date: 2026-04-07
---

# RFDB Stale Node Cleanup After Compaction

## Problem

`commit_batch(changed_files=[X], nodes=[...], ...)` is supposed to atomically
replace all nodes with `file = X` by tombstoning the old set and inserting
the new one. After compaction (RFD-20 L1 segments), this is unreliable: only
a partial subset of the old nodes gets tombstoned and the rest survive
indefinitely, contaminating queries.

This is especially visible when:

- An orchestrator step is rewritten to emit nodes with new `file` fields
  (e.g., switching DIRECTORY/FILE structural nodes from a synthetic
  `__grafema_virtual/...` path to real directory paths). The new commit
  uses upsert by semantic_id, but the OLD nodes at the synthetic path
  are not all tombstoned.
- Multiple commit_batch invocations target the same legacy file path —
  each invocation tombstones a few more nodes but never gets all of them.

## Context / Trigger Conditions

- After running a commit pipeline, `find_by_type(NODE_TYPE)` returns more
  rows than the latest commit produced.
- Graph-stream output mixes nodes with the new `file` value and a "stale"
  group with the old `file` value, both for entities you considered
  rewritten.
- `changed_files` contained the legacy path, the orchestrator log says
  `commitBatch` succeeded with `files=N` matching expectation.
- Splitting nodes by file shows orphans only at the legacy/synthetic path
  while the rest of the graph looks correct.
- Re-running the same `commit_batch` reduces the orphan count but never
  clears it to zero.
- `manifest_index.json` has a recent compaction snapshot
  (`l1_node_segments` populated) older than the failing commits.

## Diagnostic Path

1. Get a per-`file` count of the affected node type:
   ```bash
   curl -s "http://localhost:3333/api/graph-stream?nodeTypes=TYPE&maxNodes=5000" \
     | python3 -c "
import json, sys
counts = {}
for line in sys.stdin:
    try:
        d = json.loads(line)
        if d.get('type') == 'node':
            counts[d.get('file','')] = counts.get(d.get('file',''), 0) + 1
    except: pass
for k,v in sorted(counts.items(), key=lambda kv: -kv[1])[:20]: print(v, k)
"
   ```
2. Look for entries clustered under the legacy/synthetic file path that
   should have been replaced.
3. Confirm via `/api/node/<id>` that one such node has the legacy `file`
   and the metadata you remember from the old code path.
4. Confirm a corresponding "new" node exists (with the same name but new
   `file`) — proves the rewrite ran but cleanup didn't.

## Solution

### Direct cleanup via TS rfdb client (most reliable)

The TS client's `queryNodes({file: X})` uses a different storage code path
than `handle_commit_batch`'s `find_by_attr` and reliably enumerates all
matching nodes, including those promoted to L1 segments.

```js
// /tmp/cleanup-stale.mjs
import { RFDBClient } from '/path/to/grafema/packages/rfdb/dist/client.js';

const client = new RFDBClient(
  '/path/to/.grafema/rfdb.sock',  // socket path is positional arg, not options object
  'cleanup',
);
await client.connect();

const stale = [];
for await (const n of client.queryNodes({ file: '__grafema_virtual/legacy-path' })) {
  stale.push(n.id);
}
console.log('Found stale nodes:', stale.length);

for (const id of stale) {
  try { await client.deleteNode(id); }
  catch (e) { console.error('delete failed', id, e.message); }
}

await client._send('flush', {}).catch(e => console.error('flush:', e.message));
await client.close();
```

Then re-run the commit step that produces the desired state. The graph
will now be clean.

### Caveats

- **`queryNodes({file: X})` may match more than literal equality.** It
  can return any node where `file` matches the pattern under the storage
  layer's filter rules (substring or exact, depending on backend version).
  Use the **most specific** path you can to avoid wiping live data.
- **Verify the count before deleting.** If you expect ~300 stale and the
  query returns the entire graph, abort. Print first few IDs and double-
  check their type/file via `/api/node/<id>`.
- After deleting, re-commit the intended state immediately. The graph is
  in a partially-empty state until you do.

### Why not just keep calling commit_batch?

`handle_commit_batch` (rfdb_server.rs) does:

```rust
for file in &changed_files {
    let old_ids = engine.find_by_attr(&AttrQuery { file: Some(file.clone()), .. });
    for id in &old_ids { engine.delete_node(*id); }
}
```

`find_by_attr` for a `file` filter consults the in-memory `file_to_node_ids`
index, which is rebuilt from segments at startup. After compaction, some
nodes for a given file path are in L1 with index entries that can be
incomplete or stale relative to what's still on disk. Subsequent
`find_by_attr` calls return only the index-known IDs, so other historical
copies of the same node are never enumerated for tombstoning.

The TS client's `queryNodes` route through a different store method that
walks segments directly instead of trusting the file index, so it sees
the missed entries.

## Verification

After cleanup + recommit:

```bash
# Total count for the type matches what you committed
curl -s "http://localhost:3333/api/graph-stream?nodeTypes=DIRECTORY,FILE&maxNodes=2000" \
  | tail -2
# → {"edgeCount":721,"elapsed":...,"nodeCount":722,"type":"done"}

# No nodes left at the legacy path
curl -s "http://localhost:3333/api/graph-stream?nodeTypes=DIRECTORY,FILE&maxNodes=2000" \
  | python3 -c "
import json, sys
n = sum(1 for line in sys.stdin
        for d in [json.loads(line)] if d.get('type')=='node'
        and d.get('file','').startswith('__grafema_virtual'))
print('stale survivors:', n)
"
# → stale survivors: 0
```

## Notes

- **This is a workaround, not a fix.** The root fix is to make
  `find_by_attr({file:X})` consistent with `queryNodes({file:X})` after
  compaction — both should walk every storage layer (write_buffer + L0 +
  L1) and respect tombstones.
- After running this workaround, keep an eye out for the same symptom in
  future analyzes; until the root fix lands, every step that re-points a
  node's `file` field will need this cleanup.
- This bug is in the same family as `rfdb-manifest-l1-carryforward`
  (compaction interactions). Both surface as "data exists on disk but
  isn't queryable correctly."
- Don't try `delete_nodes` via REST/HTTP — only the unix-socket protocol
  exposes `deleteNode`. Use the TS client.
- Always **flush** after delete via `client._send('flush', {})` so
  subsequent queries see the tombstones.

## Related Files

- `packages/rfdb-server/src/bin/rfdb_server.rs:1996` —
  `handle_commit_batch` (the buggy delete path)
- `packages/rfdb-server/src/bin/rfdb_server.rs:1242` — `Request::DeleteNode`
- `packages/rfdb/dist/base-client.js:86` — `deleteNode` TS client API
- `packages/rfdb/dist/base-client.js:254` — `queryNodes` TS client API
- `.claude/skills/rfdb-manifest-l1-carryforward/SKILL.md` — sibling bug

Attribution

DisentinelDisentinel
View sourceMore from Disentinel →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →