agentm memory index - alexherrero/agentm GitHub Wiki

The memory index (V6-11) was the SQLite metadata table that let recall filter by kind, project, tag, and status in one query alongside the vector search. Built 2026-07-06; retired 2026-08-10 with the vector index it sat beside β€” see the amendment log. --filter survives on the corpus walk, which applies the same criteria as it goes. It lifts the queued V6 plan into a tracked design, reconciled to the built index and extended with the fingerprint column + two new memory kinds; parent agentm HLD, sibling to memory system.

AgentM Memory Index Design

Objective

Recall today runs a vector search plus a grep over each entry's frontmatter β€” there is no structured way to ask for "the security entries in project: sherwood, updated this month." V6-11 adds that: a SQLite metadata table beside the existing vector index, joined by row id, so a recall is one SQL WHERE over kind / project / tag / status AND a vector MATCH in the same query. It was the substrate the diagnostics recall ladder and token-audit's session-cost capture both waited on β€” both blockers are now cleared; each capability's own build is still ahead of it, on its own merits.

The contract for this lived only in the queued V6 plan in the vault, which no tracked design referenced β€” so four dependents read as buildable while resting on an unwritten substrate. This design lifts that plan into the governed tree, reconciles it to what the index actually is today (a four-column table awaiting additive columns), and folds in the three extensions the AG track added: the fingerprint column, and the session-cost and failure-incident memory kinds.

Overview

Markdown stays the source of truth. The index is a derived, device-local cache the recall loop reads; this slice widens its metadata so a query can filter and rank in one pass.

Markdown entries are the source of truth; the vec_index drain/full-sync walk hydrates two device-local SQLite tables joined by row id β€” entry_meta (the metadata: path, kind, status, tags, project, fingerprint, …) and entries (the FLOAT[1024] embeddings); hybrid recall runs one SQL WHERE over the metadata plus a vector MATCH, with the grep-over-frontmatter pass kept as the graceful fallback when sqlite-vec is absent; the fingerprint column feeds the diagnostics recall ladder

Markdown is the source of truth; the drain walk hydrates the two row-id-joined SQLite tables; hybrid recall is one SQL WHERE + a vector MATCH. Grep stays the graceful fallback; the fingerprint column feeds diagnostics.

Design

What is built today

The index is real and device-local (the memory system's local-index tier, never synced):

  • vec_index.py β€” a entries vec0 virtual table (FLOAT[1024]) plus a metadata table entry_meta(rowid, path, updated_at, indexed_at) β€” four columns, joined to the vectors by rowid. It has a JSONL embedding queue, an async drain, drift detection (the source of indexed_at), and a dimension-mismatch rebuild_index.
  • recall.py β€” the recall loop: tokenize β†’ vector search β†’ drift-check-and-drop β†’ grep over slug, tags, title, and the first 500 chars β†’ merge (sim Γ— 0.85 + keyword Γ— 0.05) β†’ top-5. It graceful-degrades to grep-only when sqlite-vec or embeddings are absent.

V6-11 extends this; it does not replace it.

The extended metadata table

Add columns to the built entry_meta by additive, idempotent migration (an ALTER TABLE ADD COLUMN guarded by a column check, mirroring the existing _migrate_pre_v37 step):

Column Source
kind Β· status Β· slug Β· project Β· created the entry's frontmatter
tags (JSON array) frontmatter
group_name frontmatter group (renamed β€” group is a SQL keyword)
fingerprint auto-computed by save_entry() via fingerprint.py's compute_fingerprint() β€” a sha256 hash over the normalized body β€” for every new entry; an explicit caller value (the diagnostics ladder's semantic incident key) overrides

Row id stays the join to entries. The rebuild_index contract extends to repopulate these columns from frontmatter on a full-sync, alongside the embeddings; its CREATE TABLE entry_meta must carry the new columns too, or a rebuild would drop them. The design declares which columns are indexed (kind, project, status carry a CREATE INDEX for the filter path). The built table starts at four columns, so the migration is purely additive.

Building the index from source

The index is device-local and never synced, so a fresh machine has the synced markdown but no index β€” it builds one from the vault. The markdown is the source of truth; the index is a derived cache rebuilt locally from it, the same way on a new device, after a wipe, or after a corruption.

The cold build reuses the two built operations. full_sync --rebuild walks the vault (private/ Β· projects/ Β· incubator/) and enqueues every entry; drain then embeds each one and populates both tables β€” the entries vectors and the entry_meta row, with the V6-11 metadata columns read from each entry's frontmatter in the same pass. The build is resumable: the enqueue is a plain JSONL append and the drain is cursor-backed, so an interrupted first sync continues where it left off and a re-run is idempotent.

The two halves split by dependency: enqueueing needs no sqlite-vec or embedding model (it is an append), so the walk runs anywhere; only the drain needs the local embedding model. Embedding every entry is a one-time per-device cost, and recall graceful-degrades to grep-only until the build finishes β€” so a fresh agent keeps recalling (without the vector half) while the drain catches up. The fresh index is created with the extended schema, so a cold build produces the full metadata table directly. A dedicated reindex convenience subcommand is anticipated; full_sync --rebuild is that path today.

Hybrid recall

Add a --filter path to recall.py query: an expression like tag=security AND project=sherwood compiles to one SQL WHERE over the metadata, joined with the vector MATCH, in a single query. This replaces the grep-over-frontmatter pass for the filtered case, while grep stays the graceful fallback when sqlite-vec is absent. The merge weighting is unchanged (sim Γ— 0.85 + keyword Γ— 0.05 β€” the live constants; the 0.7 / 0.3 in the comments is stale). Operator surfaces follow: /memory search --tag --project, /memory list --kind --updated-since. Out of scope (from the source plan): materializing entry bodies into SQLite, and LLM-extracted metadata.

The two new kinds + the fingerprint

kind is already a free-form kebab-case taxonomy (a path segment + a display header), so the two new kinds are reserved values of the existing kind taxonomy:

  • session-cost β€” vestigial (delivered 2026-07-07). Reserved for token-audit's session-cost capture; the Autonomy arc's observability design has now retargeted that capture off the vault onto a device-local telemetry ledger (PLAN-observability-ledger, both repo halves merged) β€” nothing writes this kind anymore. The vault entries already written stay as historical record; no data migration was needed.
  • failure-incident β€” the kind the diagnostics recall ladder writes. Its write carries a mandatory privacy scrub, because failure context is untrusted and PII-bearing β€” a persistence-boundary guard the write cannot skip.

The fingerprint is a real column, and as of PLAN-auto-org-dedup-and-lint task 1 (2026-07-18, auto-organization part 3) it has a universal writer, not just a lookup key the diagnostics ladder happens to populate: save_entry() auto-computes a stable sha256 hash over the note's normalized body (fingerprint.py's compute_fingerprint() / normalize_body() — CRLF→LF, per-line strip, whitespace-run collapse, blank-line drop, casefold; punctuation and wikilinks stay significant, deliberately favoring false negatives over false positives) for every newly written note. An explicit caller-supplied value still wins, so the diagnostics recall ladder's semantic incident join key keeps its meaning unchanged; for a failure-incident write, the mandatory PII scrub runs before the hash, so the fingerprint reflects the scrubbed body actually persisted. Best-effort — a fingerprint failure never blocks the write.

The DerivedMaintenance relationship

The memory system reserves a DerivedMaintenance extension point for exactly this kind of derived index (it points the reader at the storage seam, though the seam text does not yet carry the reservation), and nothing implements it yet β€” the built vec_index.py is a parallel working implementation. This slice builds on vec_index.py (its drain, drift detection, and rebuild already work) and names DerivedMaintenance as the contract it eventually satisfies, with a re-audit trigger so the reconciliation stays on record.

The boundary

  • vs memory system β€” the memory system owns the recall loop, the capture path, the tiers, the local-index tier, and the DerivedMaintenance extension point; this design is the indexed-recall slice it marks designed-for (the V6 milestone), specifying the metadata table + the hybrid query, and a future DerivedMaintenance implementer.
  • vs storage seam β€” the seam owns the read/write verbs, the Locator guards, and the T1/T2/T3 ownership tiers; this index is a derived device-local cache the recall path reads, beneath that contract.
  • vs the rest of V6 β€” V6-11 is the metadata table only. The hybrid-rank engine (V6-3, RRF over BM25 + vector), the typed-entity graph (V6-2), and chunking (V6-10) stay deferred; this slice is pulled forward on its own because its dependents need structured filtering; ranking stays deferred.

Dependencies

  • extends the built index β€” vec_index.py (the table + drain + rebuild) and recall.py (the query path).
  • rides the memory system's local-index tier (device-local, never synced); a future DerivedMaintenance implementer (the extension point memory-system reserves).
  • feeds diagnostics (the failure-incident kind + the fingerprint recall ladder); the session-cost kind is vestigial (see below) β€” token-audit no longer consumes it, now that PLAN-observability-ledger has landed.
  • composes privacy β€” the mandatory scrub on a failure-incident write.
  • Points up at the agentm HLD Β§Memory; the recall loop + tiers are the memory system.

Migrations

  • This is a lift. The canonical V6-11 contract lived only in the queued V6 plan in the vault (_harness/ROADMAP-AgentMemoryV6.md). This design migrates it into the governed tree, reconciled to current truth. Re-read the live source at fold in case it drifted since it was queued β€” the schema here is reconciled from the queued spec plus the AG-added axes.
  • Additive migration only. The built entry_meta is four columns; the migration is ALTER TABLE ADD COLUMN (idempotent, guarded), back-filled on the next full-sync. A clean-slate CREATE TABLE would mis-describe current truth and risk a drop-and-recreate.
  • At lift (docs): add the agentm/memory-index area to the area-taxonomy; have memory system point to this slice as the V6-11 specification; downgrade the dependents' status language from "designed" to "blocked on V6-11" where they don't already say so (the runner already does).

Risks & open questions

  • Buildability illusion. Until this lands, the diagnostics first-slice deterministic layer, token-audit session-cost capture, the runner health-check report, and the dreaming session-cost review read as independently buildable while resting on an unspecified substrate. The lift closes the spec gap and re-points those dependents.
  • The parallel-implementation tension. vec_index.py sidesteps the seam's reserved DerivedMaintenance. Building on vec_index.py is the pragmatic call, but the seam's never-implemented contract rots silently if this is left unsaid β€” hence the named re-audit trigger.
  • failure-incident is a leak surface. Its write carries untrusted, PII-bearing context; the mandatory privacy scrub is a persistence-boundary guard the write cannot skip.
  • Source-of-truth drift. The lifted spec was queued 2026-05-27; re-read the live vault plan before folding in case it changed.
  • Re-audit triggers: re-read the live V6 plan at fold; reconcile onto DerivedMaintenance if/when the seam path is implemented; flip [PENDING-IMPL] as the table + hybrid query land; revisit when V6-3 (RRF) arrives to consume the table.

Locked design calls

  • A standalone Wave-B slice. Ship the metadata table + SQL filtering + the fingerprint on their own to unblock diagnostics and token-audit; the source plan's pairing with V6-3 (RRF) is deferred β€” V6-3 consumes this table later.
  • Additive migration only. Extend the built four-column entry_meta by guarded ALTER TABLE ADD COLUMN, mirroring _migrate_pre_v37; the rebuild path's CREATE TABLE carries the new columns too.
  • group_name for the frontmatter group key. The bare group is a SQL keyword.
  • The two new kinds are reserved kind values. session-cost and failure-incident ride the existing free-form kind; the fingerprint is the one real new column.
  • Build on vec_index.py now; DerivedMaintenance is the eventual contract (re-audit trigger recorded).
  • Markdown stays the source of truth. The index is a derived, device-local cache; nothing here moves authority into SQLite.

References

  • Lifted from: the queued V6 plan (_harness/ROADMAP-AgentMemoryV6.md β€” the V6-11 metadata-table spec)
  • Extends (built): harness/skills/memory/scripts/vec_index.py (the four-column entry_meta + entries + drain + rebuild) Β· recall.py (the five-step loop + the sim Γ— 0.85 + keyword Γ— 0.05 merge) Β· fingerprint.py (the fingerprint column's universal writer β€” compute_fingerprint() / normalize_body(), wired into save_entry(); landed via PLAN-auto-org-dedup-and-lint task 1)
  • Composes: memory system (the recall loop + tiers + the local-index tier + the DerivedMaintenance reservation) Β· storage seam (the read/write verbs + ownership tiers) Β· privacy (the failure-incident scrub)
  • Consumers: diagnostics (the failure-incident + fingerprint recall ladder) Β· the runner health-check report. session-cost is vestigial β€” see the kinds section above.
  • Up: agentm HLD Β§Memory Β· memory system (the V6 indexed-recall trajectory)

Amendment log

Newest first. Collapses to one ≀2-paragraph entry at finalization; git holds the granular history.

2026-07-18 β€” fingerprint gets a universal writer (auto-organization part 3, PLAN-auto-org-dedup-and-lint task 1). The column existed since the 2026-07-06 build but had no writer of its own β€” it stayed NULL unless a caller passed one explicitly, so the schema table and the kinds section above described it as a bare lookup key the diagnostics ladder happened to populate. Task 1 of the dedup-and-lint plan adds harness/skills/memory/scripts/fingerprint.py (compute_fingerprint() / normalize_body() β€” a sha256 hash over the normalized body, conservative by design: CRLF/whitespace/case folded, punctuation and wikilinks left significant, favoring false negatives) and wires it into save_entry(), so every newly written note now gets a real content-hash fingerprint automatically. An explicit caller-supplied value still wins β€” the diagnostics ladder's semantic incident join key is unchanged β€” and for failure-incident the mandatory PII scrub runs before the hash, so the fingerprint reflects the scrubbed body actually written; best-effort, a fingerprint failure never blocks the write. This closes the "real column, no writer" gap described above; the column is no longer aspirational for ordinary entries. Not yet built: the write-time exact-duplicate dedup guard and cluster-aware collapse this fingerprint feeds (auto-organization's dedup section) β€” those are later tasks in the same plan.

2026-07-07 β€” session-cost retarget delivered (AA3, PLAN-observability-ledger agentm half). Flips the AA2 entry below from "stops being written once the plan lands" to as-built: both repo halves have merged (crickets' writer/reader repoint, agentm's runner-hosted SQLite aggregator over the resulting event log), so session-cost is now confirmed vestigial rather than merely scheduled to become so. Left as a reserved value (no data migration needed β€” the vault entries already written stay as historical record) rather than removed from the taxonomy.

2026-07-07 β€” session-cost kind marked vestigial (AA2). The Autonomy arc's observability design retargets token-audit's session-cost capture off the vault onto a device-local telemetry ledger; this kind stops being written once PLAN-observability-ledger lands the move. Left as a reserved value (no data migration needed β€” the vault entries already written stay as historical record) rather than removed from the taxonomy.

  • 2026-08-10 β€” retired with the vector stack; --filter survives on the walk. The metadata table this design specifies lived in vec_index.py's database, joined to the vector MATCH by row id β€” so when the vector stack was removed (see week-1 experiment) the table went with it. What survives: the --filter grammar and its semantics. parse_filter and _entry_matches_filter are unchanged in recall.py, and _bm25_search/_grep_search each apply the criteria as they walk β€” the path that was always the fallback when sqlite-vec was absent is now the only implementation. What is gone: _vec_search_filtered's single-query SQL join, which was the optimization this design existed to add, and the eight entry_meta columns behind it. The one non-filter dependent, dedup_guard.find_vault_duplicate, resolved a fingerprint through the same table and had no other lookup available; it is gone too, recorded in the week-1 amendment log. Why superseded rather than an edit in place: the objective section's premise β€” that a structured filter needs a SQL table beside a vector index β€” is what stopped being true, not a detail inside it. governs: drops to [] rather than moving to recall.py: the surviving grammar lives there, but agentm-memory-system.md already governs the whole harness/skills/memory/scripts/ directory, and a retired design should not hold a live file β€” the same shape recall trace already uses. Re-audit trigger: a corpus large enough that the filter walk stops fitting an interactive budget, which is the condition that would justify an index again β€” and it would be a fresh one, not this design.

  • 2026-07-06 β€” built (AG Wave B leader 3/5). _migrate_v6_11 (additive ALTER TABLE ADD COLUMN, guarded, mirrors the existing _migrate_pre_v37 pattern) + _ensure_v6_11_indexes + _extract_meta_from_file (frontmatter β†’ the eight columns, project derived from group:) ship in vec_index.py; upsert_entry populates them. The hybrid --filter path ships in recall.py (parse_filter / _entry_matches_filter / _vec_search_filtered) β€” a single SQL WHERE joined with the vector MATCH, grep-fallback preserved. session-cost and failure-incident are recognized kind values; the latter's mandatory privacy scrub ships as a self-contained privacy_scrub.py (agentm-native regex redaction β€” composing crickets' privacy capability directly would invert the one-way capability bridge, since agentm must never import crickets code). governs: now points at vec_index.py. Operator-facing CLI subcommands (/memory search --tag --project, /memory list --kind --updated-since) are not built β€” those are a thin wrapper layer above recall.py query --filter, which works end-to-end. Re-audit trigger: reconcile onto DerivedMaintenance when the seam path is implemented (unchanged from the 2026-06-26 entry below); confirm the operator-facing CLI wrapper if/when it's authored.

  • 2026-07-03 β€” drain fails loud on a missing vault instead of returning stale-looking empty stats. vec_index.py's drain subcommand now checks vault.is_dir() before calling drain_queue() and exits 1 with an error if the vault directory doesn't exist, rather than falling through to drain_queue()'s default {"processed": 0, "skipped": 0, "errors": 0, "remaining": 0} β€” a snapshot indistinguishable from a legitimately empty queue. This is a deliberate narrow exception to the module's otherwise-consistent graceful-degradation philosophy: read-only walkers like find_drifted_entries correctly stay graceful on a missing vault, but drain is a mutating operation where "vault vanished mid-run" must be distinguishable from "nothing pending." Found while writing scripts/verify-vec-index.sh's fault-injection check (R1.4 / agentmExperience#0). Re-audit trigger: if another mutating vec_index.py subcommand is added, it should get the same existence check.

  • 2026-06-28 β€” lock-down sweep (operator review). All standing fixes clean (diagram sized; no mermaid; no ADR mentions; log already newest-first). Confirmed markdown stays the source of truth, the three AG-added axes (the fingerprint column + the session-cost / failure-incident reserved kinds, the latter with the mandatory privacy scrub on write), and the hybrid --filter recall path. No content change. Locked as a v5–v8 guidepost.

  • 2026-06-26 β€” lifted the V6-11 metadata-table plan into the governed tree; approved + launched. The last Bucket-A substrate piece, and a lift: the canonical V6-11 contract lived only in the queued V6 plan in the vault, which no tracked design referenced, so its dependents rested on an unwritten substrate. This design migrates it, reconciled to the built index (the four-column entry_meta in vec_index.py, extended additively), and folds in the three AG-added axes: the fingerprint column, and the session-cost + failure-incident reserved kind values. It specifies the extended metadata table, the hybrid --filter recall path (one SQL WHERE + a vector MATCH, replacing the grep-over-frontmatter pass, grep kept as the graceful fallback), the build-from-source path (a fresh machine bootstraps the device-local index from the synced markdown via full_sync --rebuild β†’ drain, resumable, recall grep-degrades until built), and the mandatory privacy scrub on a failure-incident write. Locked: a standalone Wave-B slice (V6-3 RRF deferred); additive idempotent migration; group_name over the SQL keyword; the two kinds are reserved kind values; build on vec_index.py with DerivedMaintenance named as the eventual contract; markdown stays the source of truth. Re-audit: re-read the live V6 plan at fold; reconcile onto DerivedMaintenance when the seam path is implemented; flip [PENDING-IMPL] as the table + hybrid query land.

⚠️ **GitHub.com Fallback** ⚠️