claude_memory 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.claude-plugin/marketplace.json +1 -1
  3. data/.claude-plugin/plugin.json +1 -1
  4. data/CHANGELOG.md +25 -0
  5. data/docs/improvements.md +83 -0
  6. data/lib/claude_memory/commands/checks/fts_rank_check.rb +60 -0
  7. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  8. data/lib/claude_memory/commands/index_command.rb +22 -4
  9. data/lib/claude_memory/commands/setup_vectors_command.rb +9 -3
  10. data/lib/claude_memory/distill/null_distiller.rb +24 -2
  11. data/lib/claude_memory/hook/context_injector.rb +35 -10
  12. data/lib/claude_memory/index/lexical_fts.rb +47 -23
  13. data/lib/claude_memory/index/vector_index.rb +27 -0
  14. data/lib/claude_memory/mcp/server.rb +33 -1
  15. data/lib/claude_memory/observe/reflector.rb +32 -16
  16. data/lib/claude_memory/observe/token_overlap_matcher.rb +55 -0
  17. data/lib/claude_memory/sweep/maintenance.rb +22 -0
  18. data/lib/claude_memory/sweep/sweeper.rb +1 -0
  19. data/lib/claude_memory/version.rb +1 -1
  20. data/lib/claude_memory.rb +2 -0
  21. metadata +3 -25
  22. data/.claude/CLAUDE.md +0 -4
  23. data/.claude/memory.sqlite3 +0 -0
  24. data/.claude/output-styles/memory-aware.md +0 -1
  25. data/.claude/rules/claude_memory.generated.md +0 -87
  26. data/.claude/settings.json +0 -113
  27. data/.claude/settings.local.json +0 -59
  28. data/.claude/skills/check-memory/DEPRECATED.md +0 -29
  29. data/.claude/skills/check-memory/SKILL.md +0 -87
  30. data/.claude/skills/dashboard/SKILL.md +0 -42
  31. data/.claude/skills/debug-memory +0 -1
  32. data/.claude/skills/improve/SKILL.md +0 -631
  33. data/.claude/skills/improve/feature-patterns.md +0 -1221
  34. data/.claude/skills/memory-first-workflow +0 -1
  35. data/.claude/skills/quality-update/SKILL.md +0 -229
  36. data/.claude/skills/quality-update/implementation-guide.md +0 -346
  37. data/.claude/skills/release/SKILL.md +0 -206
  38. data/.claude/skills/review-commit/SKILL.md +0 -199
  39. data/.claude/skills/review-for-quality/SKILL.md +0 -154
  40. data/.claude/skills/review-for-quality/expert-checklists.md +0 -79
  41. data/.claude/skills/setup-memory +0 -1
  42. data/.claude/skills/study-repo/SKILL.md +0 -322
  43. data/.claude/skills/study-repo/analysis-template.md +0 -323
  44. data/.claude/skills/study-repo/focus-examples.md +0 -327
  45. data/.claude/skills/upgrade-dependencies/SKILL.md +0 -154
@@ -11,8 +11,12 @@ module ClaudeMemory
11
11
  # timer (Claude Code has no cron hook) and without extra API cost.
12
12
  #
13
13
  # Two passes, both provenance-preserving (tombstone, never hard-delete):
14
- # - dedupe: collapse near-identical active observations (same scope,
15
- # normalized body) into the newest, linking losers via consolidated_into.
14
+ # - dedupe: collapse near-duplicate active observations (same scope) into
15
+ # the newest, linking losers via consolidated_into. Similarity is decided
16
+ # by an injected matcher (default: lexical token-overlap, #73) so the
17
+ # promotion gate can actually accumulate corroboration — exact-string
18
+ # matching never folded varied wording, leaving every observation at
19
+ # corroboration 1 (the 2026-06-23 audit finding).
16
20
  # - expire_stale_info: retire info-level (🟢 / priority 3) observations
17
21
  # older than the TTL to bound context size. Important (šŸ”“) and maybe
18
22
  # (🟔) are never expired — only the lowest-signal tier ages out.
@@ -31,9 +35,10 @@ module ClaudeMemory
31
35
  end
32
36
  end
33
37
 
34
- def initialize(store, info_ttl_days: DEFAULT_INFO_TTL_DAYS)
38
+ def initialize(store, info_ttl_days: DEFAULT_INFO_TTL_DAYS, matcher: TokenOverlapMatcher.new)
35
39
  @store = store
36
40
  @info_ttl_days = info_ttl_days
41
+ @matcher = matcher
37
42
  end
38
43
 
39
44
  # @return [Result] number of observations deduped and expired
@@ -51,21 +56,36 @@ module ClaudeMemory
51
56
 
52
57
  def dedupe
53
58
  active = @store.observations.where(status: "active").order(:id).all
59
+ active.group_by { |o| o[:scope] }.sum { |_scope, rows| dedupe_scope(rows) }
60
+ end
61
+
62
+ # Greedy clustering within one scope: the newest observation in a cluster
63
+ # is the keeper; older near-duplicates fold into it. O(n²) matcher calls,
64
+ # but n is bounded (#74 cut the inflow; expire_stale_info bounds the tail).
65
+ def dedupe_scope(rows)
66
+ return 0 if rows.size < 2
67
+
68
+ ordered = rows.sort_by { |r| [r[:observed_at].to_s, r[:id]] }.reverse
69
+ folded = {}
54
70
  merged = 0
55
71
 
56
- active.group_by { |o| [o[:scope], normalize(o[:body])] }.each_value do |rows|
57
- next if rows.size < 2
72
+ ordered.each do |keeper|
73
+ next if folded[keeper[:id]]
74
+
75
+ ordered.each do |other|
76
+ next if other[:id] == keeper[:id] || folded[other[:id]]
77
+ next unless @matcher.similar?(keeper[:body], other[:body])
58
78
 
59
- keeper = rows.max_by { |r| [r[:observed_at].to_s, r[:id]] }
60
- rows.each do |loser|
61
- next if loser[:id] == keeper[:id]
62
- # Fold the loser's sightings into the keeper before tombstoning so
63
- # corroboration survives consolidation and can cross the promotion
79
+ # Fold the duplicate's sightings into the keeper before tombstoning
80
+ # so corroboration survives consolidation and can cross the promotion
64
81
  # threshold. A duplicate IS a repeated sighting.
65
- @store.increment_corroboration(keeper[:id], by: loser[:corroboration_count] || 1)
66
- @store.tombstone_observation(loser[:id], into_id: keeper[:id])
82
+ @store.increment_corroboration(keeper[:id], by: other[:corroboration_count] || 1)
83
+ @store.tombstone_observation(other[:id], into_id: keeper[:id])
84
+ folded[other[:id]] = true
67
85
  merged += 1
68
86
  end
87
+
88
+ folded[keeper[:id]] = true
69
89
  end
70
90
 
71
91
  merged
@@ -82,10 +102,6 @@ module ClaudeMemory
82
102
  ids.each { |id| @store.expire_observation(id) }
83
103
  ids.size
84
104
  end
85
-
86
- def normalize(body)
87
- body.to_s.downcase.gsub(/\s+/, " ").strip
88
- end
89
105
  end
90
106
  end
91
107
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Observe
5
+ # Default observation-similarity matcher: lexical token-overlap (Jaccard).
6
+ #
7
+ # Deterministic, free, no embedding dependency — so it runs shell-side in
8
+ # the Reflector's sweep pass at no extra cost. Two bodies are "the same
9
+ # sighting" when their significant-word sets overlap past the threshold.
10
+ # This folds the common case (one event re-observed with slightly different
11
+ # wording — "PreCompact hook set." / "PreCompact hook set — the design
12
+ # analog", Jaccard 0.6) while keeping unrelated observations apart (distinct
13
+ # developer statements share ~no content words → Jaccard ~0).
14
+ #
15
+ # It does NOT capture pure synonym paraphrases ("use SQLite" vs "chose
16
+ # SQLite") — no free lexical method can on short text (measured 2026-06-23:
17
+ # tfidf cosine 0.32 for that pair, indistinguishable from unrelated pairs
18
+ # at 0.13). For paraphrase folding, inject a semantic matcher backed by real
19
+ # embeddings: the Reflector accepts any object responding to
20
+ # `similar?(body_a, body_b)`.
21
+ class TokenOverlapMatcher
22
+ DEFAULT_THRESHOLD = 0.5
23
+
24
+ # Function words carry no episodic signal; dropping them focuses the
25
+ # overlap on subject/verb content.
26
+ STOPWORDS = %w[
27
+ a an the to of in on at for and or but we i it is are was were be been
28
+ this that these those with as by from into our your their its do does
29
+ ].to_set.freeze
30
+
31
+ def initialize(threshold: DEFAULT_THRESHOLD)
32
+ @threshold = threshold
33
+ end
34
+
35
+ # @return [Boolean] true when the two bodies are near-duplicate sightings
36
+ def similar?(body_a, body_b)
37
+ a = significant_tokens(body_a)
38
+ b = significant_tokens(body_b)
39
+ return false if a.empty? || b.empty?
40
+
41
+ intersection = (a & b).size.to_f
42
+ union = (a | b).size
43
+ (intersection / union) >= @threshold
44
+ end
45
+
46
+ private
47
+
48
+ def significant_tokens(body)
49
+ body.to_s.downcase.scan(/[a-z0-9]+/)
50
+ .reject { |word| word.length < 2 || STOPWORDS.include?(word) }
51
+ .to_set
52
+ end
53
+ end
54
+ end
55
+ end
@@ -408,6 +408,28 @@ module ClaudeMemory
408
408
  {deduped: result.deduped, expired: result.expired}
409
409
  end
410
410
 
411
+ # Self-heal the FTS5 rank index (improvement #69). Concurrent writers
412
+ # (the ingest hook vs the MCP server) can leave content_fts in a state
413
+ # where plain MATCH works but `ORDER BY rank` raises "malformed" —
414
+ # silently breaking recall while integrity_check passes. Sweep runs on
415
+ # PreCompact/SessionEnd, so probing the rank path here and rebuilding on
416
+ # failure lets recall self-repair within the session that broke it, with
417
+ # no manual `claude-memory compact`. (Detection is also surfaced by the
418
+ # doctor FtsRankCheck; this is the automatic repair.) A rebuild on a very
419
+ # large index runs to completion — corruption is rare and the rebuild is
420
+ # the only fix; the per-step budget gate keeps it from *starting* late.
421
+ # Returns: true if a rebuild was performed, false otherwise.
422
+ def repair_fts_rank
423
+ fts = ClaudeMemory::Index::LexicalFTS.new(@store)
424
+ fts.search("a", limit: 1)
425
+ false
426
+ rescue ClaudeMemory::Index::LexicalFTS::CorruptRankIndexError
427
+ fts.rebuild!
428
+ true
429
+ rescue
430
+ false
431
+ end
432
+
411
433
  # Run SQLite VACUUM to reclaim space.
412
434
  # Returns: true
413
435
  def vacuum
@@ -57,6 +57,7 @@ module ClaudeMemory
57
57
  @stats[:observations_deduped] = reflection[:deduped]
58
58
  @stats[:observations_expired] = reflection[:expired]
59
59
  end
60
+ run_if_within_budget { @stats[:fts_rank_repaired] = maintenance.repair_fts_rank }
60
61
  run_if_within_budget { @stats[:wal_checkpointed] = maintenance.checkpoint_wal }
61
62
 
62
63
  @stats[:elapsed_seconds] = Time.now - @start_time
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeMemory
4
- VERSION = "0.13.0"
4
+ VERSION = "0.13.2"
5
5
  end
data/lib/claude_memory.rb CHANGED
@@ -40,6 +40,7 @@ require_relative "claude_memory/commands/checks/reporter"
40
40
  require_relative "claude_memory/commands/checks/vec_check"
41
41
  require_relative "claude_memory/commands/checks/embeddings_check"
42
42
  require_relative "claude_memory/commands/checks/distill_check"
43
+ require_relative "claude_memory/commands/checks/fts_rank_check"
43
44
  require_relative "claude_memory/commands/help_command"
44
45
  require_relative "claude_memory/commands/version_command"
45
46
  require_relative "claude_memory/commands/doctor_command"
@@ -126,6 +127,7 @@ require_relative "claude_memory/domain/provenance"
126
127
  require_relative "claude_memory/domain/conflict"
127
128
  require_relative "claude_memory/domain/observation"
128
129
  require_relative "claude_memory/observe/observations_renderer"
130
+ require_relative "claude_memory/observe/token_overlap_matcher"
129
131
  require_relative "claude_memory/observe/reflector"
130
132
  require_relative "claude_memory/embeddings/model_registry"
131
133
  require_relative "claude_memory/embeddings/inspector"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valentino Stoll
@@ -90,30 +90,6 @@ files:
90
90
  - ".claude-plugin/scripts/hook-runner.sh"
91
91
  - ".claude-plugin/scripts/serve-mcp.sh"
92
92
  - ".claude.json"
93
- - ".claude/CLAUDE.md"
94
- - ".claude/memory.sqlite3"
95
- - ".claude/output-styles/memory-aware.md"
96
- - ".claude/rules/claude_memory.generated.md"
97
- - ".claude/settings.json"
98
- - ".claude/settings.local.json"
99
- - ".claude/skills/check-memory/DEPRECATED.md"
100
- - ".claude/skills/check-memory/SKILL.md"
101
- - ".claude/skills/dashboard/SKILL.md"
102
- - ".claude/skills/debug-memory"
103
- - ".claude/skills/improve/SKILL.md"
104
- - ".claude/skills/improve/feature-patterns.md"
105
- - ".claude/skills/memory-first-workflow"
106
- - ".claude/skills/quality-update/SKILL.md"
107
- - ".claude/skills/quality-update/implementation-guide.md"
108
- - ".claude/skills/release/SKILL.md"
109
- - ".claude/skills/review-commit/SKILL.md"
110
- - ".claude/skills/review-for-quality/SKILL.md"
111
- - ".claude/skills/review-for-quality/expert-checklists.md"
112
- - ".claude/skills/setup-memory"
113
- - ".claude/skills/study-repo/SKILL.md"
114
- - ".claude/skills/study-repo/analysis-template.md"
115
- - ".claude/skills/study-repo/focus-examples.md"
116
- - ".claude/skills/upgrade-dependencies/SKILL.md"
117
93
  - ".gitattributes"
118
94
  - ".lefthook/map_specs.rb"
119
95
  - ".mcp.json"
@@ -217,6 +193,7 @@ files:
217
193
  - lib/claude_memory/commands/checks/database_check.rb
218
194
  - lib/claude_memory/commands/checks/distill_check.rb
219
195
  - lib/claude_memory/commands/checks/embeddings_check.rb
196
+ - lib/claude_memory/commands/checks/fts_rank_check.rb
220
197
  - lib/claude_memory/commands/checks/hooks_check.rb
221
198
  - lib/claude_memory/commands/checks/reporter.rb
222
199
  - lib/claude_memory/commands/checks/snapshot_check.rb
@@ -368,6 +345,7 @@ files:
368
345
  - lib/claude_memory/mcp/tools.rb
369
346
  - lib/claude_memory/observe/observations_renderer.rb
370
347
  - lib/claude_memory/observe/reflector.rb
348
+ - lib/claude_memory/observe/token_overlap_matcher.rb
371
349
  - lib/claude_memory/otel/attributes.rb
372
350
  - lib/claude_memory/otel/constants.rb
373
351
  - lib/claude_memory/otel/ingestor.rb
data/.claude/CLAUDE.md DELETED
@@ -1,4 +0,0 @@
1
- <!-- ClaudeMemory v0.7.0 -->
2
- # Project Memory
3
-
4
- @.claude/rules/claude_memory.generated.md
Binary file
@@ -1 +0,0 @@
1
- ../../output-styles/memory-aware.md
@@ -1,87 +0,0 @@
1
- <!--
2
- This file is auto-generated by claude-memory.
3
- Do not edit manually - changes will be overwritten.
4
- Generated: 2026-06-16T18:31:16Z
5
- -->
6
-
7
- # Project Memory
8
-
9
- ## Current Decisions
10
-
11
- - From Mastra Observational Memory study: add an episodic observation layer that augments (not replaces) the dynamic-recall semantic fact store — because facts answer 'what is true' and observations answer 'what happened', and claude_memory currently lacks the episodic half; recall stays for targeted lookups.
12
- - From Mastra Observational Memory study: make observation reflection automatic via the PreCompact and SessionEnd hooks rather than a manual-only skill — because Claude Code exposes no timer/cron hook, but PreCompact fires on context pressure (the analog of Mastra's token threshold) and rides the existing session at no extra API cost.
13
- - From Mastra Observational Memory study: run the Reflector's deterministic GC shell-side in Ruby and its semantic consolidation via PreCompact additionalContext (Claude-as-reflector inline) — to keep automatic reflection within the no-extra-API-cost convention, explicitly rejecting Claude Code Routines and subagents because each incurs a separate token budget.
14
- - From Mastra Observational Memory study: tombstone superseded observations via a consolidated_into link rather than hard-deleting them (unlike Mastra's lossy drop) — to preserve claude_memory's provenance guarantee while still bounding context size.
15
- - From Mastra Observational Memory study: promote an observation to a structured fact only after corroboration across multiple observations — because requiring repeated sightings before commitment doubles as an anti-hallucination gate against reject-churn from one-off doc/example text.
16
- - MCP tool-call telemetry stores minimal columns (tool_name, duration_ms, result_count, scope, error_class) — deliberately no query_text or query_hash. YAGNI: hashes are write-only without the raw text, and raw text adds privacy concerns without clear value beyond existing shortcut tools (memory.decisions, memory.conventions). — to avoid storing query hashes that are write-only without the raw text.
17
- - From QMD 2026-02-02 restudy: adopt Claude Code plugin format, MCP structured content pattern, MCP query guide prompt, inline status checks. Carry forward sqlite-vec, RRF, docids, smart expansion from 2026-01-26. Reject custom fine-tuned models, LLM reranking, YAML collections. — to align with our pure-Ruby/SQLite constraints (no GGUF, no LLM-side fine-tuning).
18
- - From claude-supermemory study: adopt SessionStart hook context injection (hookSpecificOutput.additionalContext), tool-specific observation compression, and relative time formatting. Reject cloud storage dependency and no-test approach. — to avoid cloud-storage lock-in and no-test fragility identified in the study.
19
-
20
- ## Conventions
21
-
22
- - A/B testing methodology for memory plugin evaluation — How to test with/without memory using claude CLI — --plugin-dir doesn't work with --bare, use --mcp-config instead — To A/B test memory's impact on Claude Code responses: (imported from project auto-memory; see source file for full reasoning) — to enable head-to-head measurement of memory's impact on Claude Code responses.
23
- - do...end blocks over braces when args repeat or block is non-trivial — Block syntax preference for multi-argument or multi-expression blocks — When a block call has repeated argument names, multiple expressions, or reads awkwardly on one line, use `do...end` form rather than `{ }`. One-liners with simple single-expression bodies and short argument lists are fine with braces. (imported from project auto-memory; see source file for full reasoning) — to keep multi-line blocks readable and avoid brace/multi-expression awkwardness.
24
- - Always commit .claude/memory.sqlite3 — Per user direction (2026-05-21), .claude/memory.sqlite3 should be staged and committed alongside any change that updates project memory — even though it's a binary SQLite DB with WAL artifacts. — Always include `.claude/memory.sqlite3` in commits that touch project memory or knowledge. (imported from project auto-memory; see source file for full reasoning) — to ensure project memory is reproducible across collaborators.
25
- - Commit workflow preferences — How the user prefers commits to be structured and when to make them — Wait for the user to ask for commits — don't commit proactively. When asked, group changes into logical atomic commits: (imported from project auto-memory; see source file for full reasoning)
26
- - Data-driven analysis before design changes — User expects thorough multi-project data surveys and critical questioning of assumptions before committing to architectural changes — When proposing design changes (especially to schemas, vocabularies, or policies), gather real usage data first and present a critical analysis before implementing. (imported from project auto-memory; see source file for full reasoning) — to avoid premature architectural commitments before real usage data is in hand.
27
- - Fix hallucination triggers at the source, not via repeated reject churn — When the distiller repeatedly produces the same wrong fact, trace it to the CLAUDE.md / docs example text; fixing the source stops re-appearance — When the dashboard's Conflicts tab accumulates clusters of the same kind of bad fact (e.g. many `uses_database` contradictions against `sqlite`), the root cause is almost always **example text in documentation** that the distiller is interpreting as a literal claim about the current repo. Single-value predicates (`uses_database`, `deployment_platform`, `auth_method`) are especially vulnerable becau... (imported from project auto-memory; see source file for full reasoning) — because reject-only is treating symptoms; the root cause is doc/example text the distiller mistakes for project claims.
28
- - Hooks run the installed gem, not the working copy — always `rake install` after editing hook/MCP code — .claude/settings.json hooks invoke `claude-memory` via PATH, so changes on a branch only take effect after `bundle exec rake install` — `.claude/settings.json` hooks call bare `claude-memory hook ingest` / `claude-memory hook context` / etc. That resolves via PATH to the installed gem, not the working-copy `./exe/claude-memory`. After editing any hook/MCP/distiller code on a branch, the change does NOT reach Claude Code until `bundle exec rake install` rebuilds and reinstalls the gem (which overwrites the prior install at the same ... (imported from project auto-memory; see source file for full reasoning) — to ensure code changes reach the production hooks, which invoke the installed gem via PATH.
29
- - No extra API costs for features — User strongly prefers using Claude Code itself (skills, context hooks) over separate API calls that cost extra money — Do not add features that require separate Anthropic API calls (e.g., via anthropic-rb gem) when Claude Code itself can perform the same task. Use skills, context hook injection, and MCP tools to leverage the existing Claude Code session instead. (imported from project auto-memory; see source file for full reasoning) — to avoid hidden ongoing spend that bypasses the Claude Code session's existing budget.
30
- - Quality review update cycle — Keep quality_review.md current as items are resolved — don't let it drift — When completing quality review items, update `docs/quality_review.md` immediately: (imported from project auto-memory; see source file for full reasoning) — to prevent quality_review.md from drifting out of date.
31
- - Refactoring approach preferences — How to approach god object extraction and structural refactoring in this codebase — Use module inclusion (not class extraction) when breaking up god objects. Include modules directly into the existing class so the public API is unchanged and zero tests need modification. This was validated three times:
32
- - Round-trip migration specs cover each prior release boundary — For pre-release prep, write end-to-end migration specs from every distinct schema boundary back through ~3 prior releases — Before cutting a release that includes migrations, add round-trip specs that fixture an older DB at each distinct prior release's schema version, open via `SQLiteStore.new`, and assert the full upgrade path: schema_info advancement, data preservation across entities/facts/content_items/provenance, additive table/column creation, predicate-rewrite effects where applicable, and idempotency on re-open... (imported from project auto-memory; see source file for full reasoning) — to ensure migrations remain forward-compatible across release boundaries.
33
- - Codify behavioral contracts in tests, not just comments — When code has a deliberate scope limitation (one-shot, advisory-only, intentionally non-idempotent), write a test that fails if someone "fixes" it into being more general — When code has a deliberate scope limitation — a one-shot data migration, an advisory-only field, a method intentionally not idempotent for new inputs — write a test that exercises a scenario which would *break* if someone tried to make it more general. (imported from project auto-memory; see source file for full reasoning)
34
- - Treat UX gaps as architecture smells — user inspection/debugging questions expose god classes and missing abstractions — When users ask "can I see/debug/act on X in the dashboard?", the answer is almost always "we need a new class or route, not a new button" — Across three architectural reviews in the 2026-04-17 → 20 session, every concrete UX gap the user identified traced back to a structural issue the code already had, not a frontend-only fix. Treating critique as a forcing function for refactoring produced cleaner results than either extracting preemptively (premature) or patching only the surface (symptom). (imported from project auto-memory; see source file for full reasoning) — because frontend patches usually mask structural debt that surfaces later.
35
- - "database disk image is malformed" from FTS5 `ORDER BY rank` after sqlite3 .recover — sqlite3 .recover restores rows but can leave contentless FTS5 auxiliary indexes in a state where basic MATCH works but ORDER BY rank throws "malformed"; fix is `claude-memory compact` to rebuild the FTS index — A DB recovered via `sqlite3 corrupt.db .recover > dump.sql && sqlite3 fresh.db < dump.sql` can end up with an FTS5 index that's *partially* functional: (imported from project auto-memory; see source file for full reasoning) — fix is claude-memory compact to rebuild the FTS index without doing another .recover.
36
- - `rake install` uses `git ls-files`; untracked files silently disappear from the gem — Running `bundle exec rake install` before staging new files produces a gem missing those files, causing LoadError in hooks and MCP server — The claude_memory gemspec builds its file list via `IO.popen(%w[git ls-files -z], ...)` (claude_memory.gemspec:24). Any file that hasn't been `git add`ed at build time is **invisible to the gem** even though it exists on disk. The local working copy keeps running fine (dashboard server uses `./exe/claude-memory` against the repo directly), but the installed gem at `~/.gem/ruby/*/gems/claude_memory-... (imported from project auto-memory; see source file for full reasoning) — to ensure new files reach the installed gem (the gemspec uses git ls-files for the manifest).
37
- - Distiller scope_hint is advisory, not a routing signal — NullDistiller emits scope_hint: "global" for text matching GLOBAL_SCOPE_PATTERNS, but the resolver never routes writes between stores — scope_hint must not override fact.scope — `Distill::NullDistiller#global_scope_signal?` matches text like "always" / "my preference" / "in all projects" and stamps `scope_hint: "global"` on every fact extracted from that text. The hint is advisory metadata for downstream promotion decisions. It is NOT a routing signal — the resolver writes to whichever `SQLiteStore` was injected into it (always the project DB in the normal ingest path), re... (imported from project auto-memory; see source file for full reasoning) — to prevent scope drift between resolver-determined store and distiller-stamped hint.
38
- - Sequel DB reads must use the extralite adapter — Opening a SQLite DB for ad-hoc reads requires the extralite adapter URI; Sequel.sqlite silently depends on an ungem'd sqlite3 — Never use `Sequel.sqlite(db_path)` or `Sequel.sqlite(db_path, readonly: true)` in this codebase. The gemspec lists only `extralite (~> 2.14)` — it does **not** depend on the `sqlite3` gem. `Sequel.sqlite` routes through Sequel's `sqlite` adapter which requires `gem "sqlite3"` and will raise `Sequel::AdapterNotFound: LoadError: cannot load such file -- sqlite3` at runtime. (imported from project auto-memory; see source file for full reasoning) — to avoid Sequel::AdapterNotFound at runtime (this gem doesn't depend on the sqlite3 adapter).
39
- - Never `git checkout --` an active SQLite DB with WAL mode — Using `git checkout --` on .claude/memory.sqlite3 while readers/writers are open corrupts the DB via WAL/main file mismatch — Never run `git checkout -- .claude/memory.sqlite3` (or any SQLite DB in WAL mode) while any process has it open. Git replaces only the main DB file, leaving the WAL/SHM sidecar files referencing pages that no longer exist in the replaced file. Next read → "Extralite::Error: database disk image is malformed" and integrity_check shows btree errors across multiple trees. (imported from project auto-memory; see source file for full reasoning) — to avoid WAL/main file mismatch corruption.
40
- - SQLiteStore silently creates in-memory DB for relative paths — `SQLiteStore.new('.claude/memory.sqlite3')` with a relative path opens an empty in-memory DB, not the file — always pass absolute paths in tests/probes — `SQLiteStore.new(path)` builds a Sequel URI as `extralite:#{path}`. With a relative path like `.claude/memory.sqlite3`, the resulting URI `extralite:.claude/memory.sqlite3` is parsed with an empty database component, so Extralite opens an in-memory database. Schema migrations run against the in-memory DB (so `schema_version` reports the current version), but ALL queries return 0 rows and the real f... (imported from project auto-memory; see source file for full reasoning) — to ensure SQLiteStore opens the real file rather than a silent in-memory shadow.
41
- - Two tool_calls tables exist — don't conflate them — tool_calls (v3) stores transcript-observed Claude Code tool usage; mcp_tool_calls (v13) stores MCP server telemetry — There are **two** tables with similar names serving different purposes: (imported from project auto-memory; see source file for full reasoning) — to avoid joining unrelated rows from disjoint telemetry tables.
42
- - Distiller hallucination from CLAUDE.md example text — The scope-system example in CLAUDE.md causes recurring false fact extraction — reject + re-ingest creates rejection churn — CLAUDE.md contains a scope-system explanation with example text: (imported from project auto-memory; see source file for full reasoning) — to prevent re-extraction churn from documentation example text that the distiller takes literally.
43
- - PredicatePolicy is the single source of truth for predicate vocabulary — All predicate knowledge (vocabulary, cardinality, sections, synonyms, LLM guidance) derives from PredicatePolicy — never hardcode predicate names elsewhere — As of 0.9.0, `PredicatePolicy` in `lib/claude_memory/resolve/predicate_policy.rb` is the authoritative source for all predicate-related behavior. This was a deliberate consolidation after finding the same predicate list duplicated in 4 files that drifted independently. (imported from project auto-memory; see source file for full reasoning) — to avoid divergent predicate lists across files that drifted independently before consolidation.
44
- - Hook-telemetry features need a manual hook trigger to verify in production, not just specs — `bundle exec rake install` AND fire a real hook AND check `sqlite3 ... json_extract(detail_json, '$.<field>')`, because specs assert against working-tree code but `.claude/settings.json` hooks invoke the installed gem via PATH so the asserted field can be silently absent in production. Hit on 2026-04-30 shipping #47 token-budget telemetry: 156 specs green but `context_tokens` was missing from 24h of real activity_events.
45
- - When verifying any new field on activity_events.detail_json, the canonical smoke test is: `echo '{"hook_event_name":"SessionStart","session_id":"smoketest","source":"startup","cwd":"$(pwd)"}' | claude-memory hook context` then inspect via `sqlite3 .claude/memory.sqlite3 "SELECT json_extract(detail_json, '$.<field>') FROM activity_events WHERE event_type='hook_context' ORDER BY id DESC LIMIT 1"`. If null after rake install, the installed gem code hasn't picked up the change. — to ensure the installed gem actually picks up new detail_json fields.
46
- - Treat UX gaps as architecture smells: when a user asks "can I see/debug/act on X in the dashboard?" the answer is almost always a missing class or route, not a new button. Every UX critique in this project's dashboard work traced to a structural gap — god-class growth, four drifting fact serializers, scope_hint as silent scope override, no fact detail endpoint. Pattern: the surface question is usually a router into the architecture. Before reaching for frontend fixes, ask "what server-side data shape would make this easy?" — if that shape doesn't exist cleanly, treat it as the real work. Commit refactor separately from feature it enables ([Refactor]/[Feature]/[Fix] prefixes) so the critique→structural fix→UX fix chain is visible in git log.
47
- - Four-surface staleness: after any change that touches UI + backend + plugin-launched binaries, refresh all four or the change looks broken. (1) bundle exec rake install so the installed gem catches up (hooks + MCP launched by Claude Code run from PATH). (2) Ctrl-C and re-run ./exe/claude-memory dashboard — server is long-lived Ruby, no live-reload. (3) /mcp reconnect in Claude Code so the MCP subprocess respawns. (4) Hard-refresh browser (Cmd-Shift-R) so cached index.html JS reloads. Skipping any produces confusing "my fix doesn't work." rspec green does NOT mean end-to-end works; before declaring UI-affecting changes done, curl the endpoint and verify shape matches frontend expectation. curl alt-port dashboard (--port 3388 --no-open in background) is fastest smoke test without disturbing user's running dashboard.
48
- - MCP tool-call telemetry is recorded via MCP::Telemetry wrapping Server#handle_tools_call. Writes to mcp_tool_calls table in the project DB. Swallows DB errors so telemetry never breaks a real tool response. Viewable via 'claude-memory stats --tools [--since DAYS]'. — to ensure telemetry never breaks a real tool response (DB errors are swallowed).
49
- - mcp_tool_calls retention is 90 days, enforced by Sweep::Maintenance#prune_old_mcp_tool_calls wired into Sweeper#run!. Configurable via mcp_tool_call_retention_days in Maintenance DEFAULT_CONFIG. — long enough to support quarterly tool-usage retrospectives without unbounded DB growth.
50
- - CLAUDE_CONFIG_DIR env var overrides the default ~/.claude location for Configuration#global_db_path. Access via Configuration#claude_config_dir. Additive, backwards compatible. — to support test isolation and air-gapped installs that don't touch ~/.claude.
51
- - Registry::COMMANDS stores {class:, description:} entries as the single source of truth for command name, class reference, and shell-completion description. Class constants stored directly (no const_get). Registry.descriptions feeds CompletionCommand; adding a new command requires updating only this hash. — to ensure adding a command requires only this hash update (no const_get, no description duplication).
52
- - EXPECTED_HOOKS constant must stay in sync with events in HooksConfigurator#build_hooks_config. Adding a new hook event without updating EXPECTED_HOOKS causes false doctor warnings. — without this sync the doctor reports false hook-mismatch warnings.
53
- - Tests using --db for hook context only set the project DB path. StoreManager still connects to the real global DB. Must stub Configuration to return a temp global path for isolation. — to ensure test isolation from the user's real global DB.
54
- - Version must be updated in three places: lib/claude_memory/version.rb, .claude-plugin/plugin.json, .claude-plugin/marketplace.json. Runtime code uses ClaudeMemory::VERSION dynamically. — to ensure plugin/marketplace metadata matches the gem version on release.
55
- - Use module inclusion (not class extraction) to break up god objects — preserves public API so zero tests need modification — to avoid breaking the public API and forcing test updates during god-object cleanup.
56
- - Configuration class has instance methods only — use Configuration.new.global_db_path, not Configuration.global_db_path. Stub with instance_double + allow(Configuration).to receive(:new).and_return(config) — to make Configuration stubbable in tests via instance_double.
57
- - OperationTracker.reset_stuck_operations only resets operations older than 24 hours (STALE_THRESHOLD_SECONDS). Tests must backdate started_at to trigger resets. — to prevent the reset from clobbering operations still legitimately in flight.
58
- - SCHEMA_VERSION constant lives in Store::SchemaManager module but is accessible as SQLiteStore::SCHEMA_VERSION via Ruby include-based constant lookup — accessible via include because Ruby's constant lookup walks the inclusion chain.
59
- - MCP tools return dual content (text summary) + structuredContent (JSON) via TextSummary module and Server#handle_tools_call. Compact mode (compact: true) omits receipts for ~60% smaller responses. — so that compact-mode callers pay ~60% fewer tokens per response.
60
- - ContentSanitizer strips system-reminder, local-command-caveat, command-message, command-name, command-args tags in addition to private/no-memory/secret/claude-memory-context. — to prevent harness-internal tags from being ingested as user content.
61
- - Core::RelativeTime module provides progressive time formatting: just now → Xm ago → Xh ago → Xd ago → YYYY-MM-DD. Used in ResponseFormatter for *_ago fields. — to make stale-fact warnings legible across both recent and old timestamps.
62
- - MCP server registers memory_guide prompt via prompts/list and prompts/get endpoints. QueryGuide module holds prompt content. — to ensure memory_guide is discoverable via the standard MCP prompt registry.
63
-
64
- ## Technical Constraints
65
-
66
- - **Uses framework**: django
67
- - **Uses language**: typescript
68
- - **Uses language**: python
69
- - **Uses framework**: rails
70
- - **Uses framework**: react
71
- - **Uses framework**: sinatra
72
- - **Uses language**: javascript
73
- - **Uses language**: go
74
- - **Uses language**: ruby
75
- - **Uses database**: sqlite
76
-
77
- ## Additional Knowledge
78
-
79
- ### Architecture
80
-
81
- - mcp_server: Claude Code does NOT pass its session_id into plugin-spawned MCP server subprocesses — neither via JSON-RPC transport nor CLAUDE_SESSION_ID env var. Configuration.new.session_id returns nil inside the MCP process, so MCP-originated activity events (recall, store_extraction) get session_id=nil. Hook commands are different — .claude/settings.json payloads explicitly include session_id in their JSON. For dashboards or any feature that needs per-session attribution of MCP-originated events, correlate by time window using hook events (which do carry session_id) rather than strict session_id equality. Dashboard::API#efficacy uses session_window + within_window? for this.
82
- - MCP::Tools: Thin 104-line dispatcher that includes 6 handler modules in mcp/handlers/: QueryHandlers, ShortcutHandlers, ContextHandlers, ManagementHandlers, StatsHandlers, SetupHandlers
83
- - Recall: 94-line facade delegating to @engine (DualEngine or LegacyEngine), both include shared QueryCore module with all store-level query logic
84
- - SQLiteStore: 386-line CRUD class that includes RetryHandler (retry/connection logic) and SchemaManager (migrations/version sync) modules
85
- - Embeddings: Pluggable providers via Embeddings.resolve(name, env:). Three providers: tfidf (default), fastembed, api. Duck-typed contract: name, dimensions, generate(text). ENV: CLAUDE_MEMORY_EMBEDDING_PROVIDER
86
- - Embeddings::DimensionCheck: Pure value object — DimensionCheck.call(store, provider) returns Data.define Result with :fresh/:match/:mismatch status. No side effects; caller decides how to handle mismatch.
87
-
@@ -1,113 +0,0 @@
1
- {
2
- "hooks": {
3
- "Stop": [
4
- {
5
- "hooks": [
6
- {
7
- "type": "command",
8
- "command": "claude-memory hook ingest --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
9
- "timeout": 5,
10
- "statusMessage": "Saving memory..."
11
- }
12
- ]
13
- }
14
- ],
15
- "StopFailure": [
16
- {
17
- "hooks": [
18
- {
19
- "type": "command",
20
- "command": "claude-memory hook ingest --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
21
- "timeout": 5,
22
- "statusMessage": "Saving memory..."
23
- }
24
- ]
25
- }
26
- ],
27
- "SessionStart": [
28
- {
29
- "hooks": [
30
- {
31
- "type": "command",
32
- "command": "claude-memory hook context",
33
- "timeout": 5,
34
- "statusMessage": "Loading memory..."
35
- }
36
- ]
37
- }
38
- ],
39
- "PreCompact": [
40
- {
41
- "hooks": [
42
- {
43
- "type": "command",
44
- "command": "claude-memory hook ingest --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
45
- "timeout": 30,
46
- "statusMessage": "Saving memory..."
47
- },
48
- {
49
- "type": "command",
50
- "command": "claude-memory hook sweep --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
51
- "timeout": 30,
52
- "statusMessage": "Sweeping memory..."
53
- }
54
- ]
55
- }
56
- ],
57
- "SessionEnd": [
58
- {
59
- "hooks": [
60
- {
61
- "type": "command",
62
- "command": "claude-memory hook ingest --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
63
- "timeout": 30,
64
- "statusMessage": "Saving memory..."
65
- },
66
- {
67
- "type": "command",
68
- "command": "claude-memory hook sweep --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
69
- "timeout": 30,
70
- "statusMessage": "Sweeping memory..."
71
- }
72
- ]
73
- }
74
- ],
75
- "TaskCompleted": [
76
- {
77
- "hooks": [
78
- {
79
- "type": "command",
80
- "command": "claude-memory hook ingest --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
81
- "timeout": 10,
82
- "statusMessage": "Saving memory..."
83
- }
84
- ]
85
- }
86
- ],
87
- "TeammateIdle": [
88
- {
89
- "hooks": [
90
- {
91
- "type": "command",
92
- "command": "claude-memory hook ingest --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
93
- "timeout": 15,
94
- "statusMessage": "Saving memory..."
95
- }
96
- ]
97
- }
98
- ],
99
- "Notification": [
100
- {
101
- "matcher": "idle_prompt",
102
- "hooks": [
103
- {
104
- "type": "command",
105
- "command": "claude-memory hook sweep --db /Users/valentinostoll/src/claude_memory/.claude/memory.sqlite3",
106
- "timeout": 10,
107
- "statusMessage": "Sweeping memory..."
108
- }
109
- ]
110
- }
111
- ]
112
- }
113
- }
@@ -1,59 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "mcp__claude-memory__memory_changes",
5
- "mcp__claude-memory__memory_status",
6
- "mcp__claude-memory__memory_recall",
7
- "Bash(./exe/claude-memory:*)",
8
- "mcp__claude-memory__memory_store_extraction",
9
- "mcp__plugin_claude-memory_claude-memory__memory_store_extraction",
10
- "mcp__plugin_claude-memory_claude-memory__memory_recall",
11
- "mcp__plugin_claude-memory_claude-memory__memory_status",
12
- "mcp__plugin_claude-memory_memory__memory_recall",
13
- "mcp__memory__memory_recall",
14
- "mcp__memory__memory_changes",
15
- "mcp__memory__memory_status",
16
- "mcp__memory__memory_store_extraction",
17
- "mcp__memory__memory_explain",
18
- "Bash(bundle exec rspec:*)",
19
- "Bash(bundle exec rake:*)",
20
- "mcp__plugin_claude-memory_memory__memory_store_extraction",
21
- "WebFetch(domain:raw.githubusercontent.com)",
22
- "Bash(grep:*)",
23
- "Bash(gem list:*)",
24
- "Bash(ag:*)",
25
- "Bash(git log:*)",
26
- "Bash(find:*)",
27
- "Bash(wc:*)",
28
- "mcp__plugin_claude-memory_memory__memory_architecture",
29
- "mcp__plugin_claude-memory_memory__memory_recall_index",
30
- "Bash(./bin/run-evals:*)",
31
- "WebSearch",
32
- "mcp__memory__memory_check_setup",
33
- "WebFetch(domain:docs.anthropic.com)",
34
- "Bash(export PATH=\"$HOME/.bun/bin:/usr/bin:/bin:$PATH\")",
35
- "Bash(qmd search:*)",
36
- "Skill(study-repo)",
37
- "WebFetch(domain:www.rubydoc.info)",
38
- "Bash(git status:*)",
39
- "WebFetch(domain:github.com)",
40
- "mcp__memory__memory_stats",
41
- "mcp__memory__memory_recall_index",
42
- "Bash(mkdir -p /tmp/study-repos)",
43
- "Read(//tmp/**)",
44
- "Bash(ls:*)",
45
- "Bash(sort -k2 -rn)",
46
- "Bash(claude-memory recall:*)",
47
- "Bash(claude-memory stats:*)",
48
- "Bash(claude-memory search:*)",
49
- "Bash(bundle exec:*)",
50
- "Skill(improve)",
51
- "Skill(improve:*)",
52
- "WebFetch(domain:arxiv.org)",
53
- "mcp__memory__memory_conflicts"
54
- ]
55
- },
56
- "enableAllProjectMcpServers": false,
57
- "disabledMcpjsonServers": ["memory"],
58
- "outputStyle": "memory-aware"
59
- }
@@ -1,29 +0,0 @@
1
- # Deprecated: /check-memory
2
-
3
- This skill is **no longer needed** and should not be used.
4
-
5
- ## Why Deprecated?
6
-
7
- The `/check-memory` skill was created to force a "check memory before file exploration" workflow. However, this should be **automatic**, not manual.
8
-
9
- ## What Replaced It?
10
-
11
- The enhanced `memory-aware` output style now handles this automatically by:
12
- - Explicitly instructing Claude to check memory FIRST before file reads
13
- - Providing clear workflow: memory.recall → then file exploration if needed
14
- - Making this behavior persistent across all conversations
15
-
16
- ## If You Need Debugging
17
-
18
- Use `/debug-memory` instead to troubleshoot ClaudeMemory installation issues.
19
-
20
- ## Migration
21
-
22
- If you were using `/check-memory`:
23
- - **Remove** any references to it
24
- - **Use** the `memory-aware` output style (automatically applied)
25
- - **Trust** that Claude will check memory first automatically
26
-
27
- ## Archive Date
28
-
29
- Deprecated: 2026-01-29