note-path-not-identity
Summary
Section titled “Summary”This session took the vir CLI (@djolex999/vir-cli, an LLM-wiki tool that distills Claude Code transcripts into an Obsidian vault) from a status review through a run of fixes and features, ending at v0.17.8. The main lesson is that vir’s state was split between files and SQL: a note’s path, .rejected/ moves, and Related links were all derived from mutable topic slugs or file locations, so many DB-backed read paths and rewrites silently lost or resurrected data. writer.ts locateBySession() and the pruned_at DB gate are the fixes that carry this.
What Was Learned
Section titled “What Was Learned”- A note’s path is not its identity.
makeSlug(topic, sessionId)insrc/pipeline/slug.tsderives the filename from the topic, and the distiller retitles on purpose (0.9.1). Anything that recomputed the path from the current topic missed the existing note. That lost theverifiedflag and its +0.2 retrieval boost (bug-hunt #12), left duplicates, and undid rejection (#9). 0.17.3 replaced this withlocateBySession(), which indexes files by the 8-char session suffix. - The suffix is only a hint.
makeSlugtruncates the session id to 8 chars, so two sessions can collide, and a match authorises deleting the old file. The first draft deleted the colliding note, and a collision test caught it. Every hit is now confirmed against the fullsession_idin frontmatter. A collision degrades to an orphaned duplicate, never a deletion. - A file move is not a retrieval gate.
vir reviewrejects by moving a note to.rejected/, which SQL cannot see. It leaks through eight DB-backed paths:listDistilledand everything downstream of it,getStats, and the embedding sweep. Search only excludes rejected notes by accident, because the moved file reads as empty content. The review leak was filed, not fixed. - Prune state is DB-authoritative.
vir pruneusespruned_atandprune_reasonon the sessions row, chosen over two alternatives:skipped, which repeats the 0.14.0 semi-prune.- Frontmatter, which SQL cannot see.
- Every serving query carries
prunedGate(), a no-op when the column is absent so the read-only MCP path doesn’t break. - A gate at the paid boundary in
run.tsstops--fullre-billing pruned sessions. --restoreis byte-exact because content, embedding and hash are kept.
- Classification had to be pure and zero-I/O. 396 of 411 distilled transcripts are already deleted from disk, and
entrypointis NULL on 376 of 411 rows because it was never backfilled. So the stored path (subagents/,wf_) does the work.unclassifiable(231 rows) andmerge-winner(12 rows) are reported and never pruned.- Live result: 132 demoted, 268 serving.
- A rewrite with no embedder used to wipe every Related section.
neighborLinksneeds a vector. With no provider it computes zero neighbours, which renders the same as a note that has none. 0.17.6 makeswrite()fall back topreservedRelatedSection. Dangling links after a prune clear by regenerating withvir run --rewrite-only, which needs a working embedder. That took 177 dangling links to 0 in live notes. - Stray files are TF-IDF-retrievable. Pre-0.17.3 retitle debris (32 files) is skipped by the embedding path but indexed by the TF-IDF walk, which skips only
summaries,.rejectedandarchived. So one session can be cited twice when the embedder is down.vir lint --straysfinds them.- The check asks whether any non-pruned row can produce the filename, and ignores the
contentcolumn. - A session awaiting
vir reconcilehas empty content while its file is the only copy. A content-based test flagged that live note as debris.
- The check asks whether any non-pruned row can produce the filename, and ignores the
- Silent recorded failures are invisible ones. 14 sessions died in one
fetch failedwindow on 2026-06-11 and were found months later, after Claude Code deleted the transcripts. 0.17.8 adds two things:- A desktop notification at the end of any errored run (
failureNotice). - A doctor
distill failuresrow whose severity keys on recoverability (transcript still exists), not count. - Human table only; the 8-field
doctor --jsoncontract is unchanged.
- A desktop notification at the end of any errored run (
- Audit-backlog fixes (0.17.4):
- #17: a garbled classify response no longer buries a session.
parseClassificationmarksunparsedandrun()throwsClassifyParseError, so the row goes throughrecordErrorand stays retryable, bounded byMAX_DISTILL_ATTEMPTS. - #13:
applyPlanin the CLAUDE.md updater is now fence-aware and refuses unbalanced markers. An orphan START used to arm a later deletion of user prose. - #14: a pricing override for a model missing from
DEFAULT_PRICINGused to log $0. - #19: the MCP server reported a hardcoded
0.1.1and 4 of 6 tools, andgetStatscrashed on an un-migrated DB.
- #17: a garbled classify response no longer buries a session.
- Verification traps hit this session. All three were caught by checking the result:
- Changing a function’s return type to an object made a refusal print as a success tick, because an object is truthy, and
tscwas fine with it. - A
sedthat bumped the test count no-opped because the target number had already moved. - A test that assumed Ollama was absent broke once Ollama ran. It now sets
embeddingProvider: "none".
- Changing a function’s return type to an object made a refusal print as a success tick, because an object is truthy, and
- Ollama wasn’t auto-starting. The
.applogin item was disabled,brew serviceswas unregistered, and two installs coexisted.brew services start ollamafixed it, and the.applogin item should stay off so two servers don’t race for port 11434. - Release process.
npm publishneeds a 2FA one-time password, so the publish itself was left to the user. The registry sat at 0.17.1 whilemainand the tags were seven releases ahead.- A tag was force-moved to match
mainbefore publishing, and it is worth verifying that alignment first. - A repeat publish attempt returns 403 because a version number cannot be reused.
Context
Section titled “Context”The session opened as a status and roadmap review of vir. It found the issue tracker and audit docs stale, then turned into fixing what the verification surfaced. The work followed test-first, using one PR per finding. The later steps applied the prune and rewrite to the user’s live vault, with DB and vault snapshots taken first. The full-vault stray cleanup was left to the user because the auto-mode classifier blocked the mutating run.
Related
Section titled “Related”- vir-audit-subagent-development
- test-isolation-leak
- schema-enumeration-stops-drops
- silent-database-failure-on-success-path
- trailing-path-delimiter-cwd-search