claude_memory 0.13.1 → 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 (41) 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 +15 -0
  5. data/docs/improvements.md +4 -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/index/lexical_fts.rb +47 -23
  11. data/lib/claude_memory/index/vector_index.rb +27 -0
  12. data/lib/claude_memory/mcp/server.rb +33 -1
  13. data/lib/claude_memory/sweep/maintenance.rb +22 -0
  14. data/lib/claude_memory/sweep/sweeper.rb +1 -0
  15. data/lib/claude_memory/version.rb +1 -1
  16. data/lib/claude_memory.rb +1 -0
  17. metadata +2 -25
  18. data/.claude/CLAUDE.md +0 -4
  19. data/.claude/memory.sqlite3 +0 -0
  20. data/.claude/output-styles/memory-aware.md +0 -1
  21. data/.claude/rules/claude_memory.generated.md +0 -87
  22. data/.claude/settings.json +0 -113
  23. data/.claude/settings.local.json +0 -59
  24. data/.claude/skills/check-memory/DEPRECATED.md +0 -29
  25. data/.claude/skills/check-memory/SKILL.md +0 -87
  26. data/.claude/skills/dashboard/SKILL.md +0 -42
  27. data/.claude/skills/debug-memory +0 -1
  28. data/.claude/skills/improve/SKILL.md +0 -631
  29. data/.claude/skills/improve/feature-patterns.md +0 -1221
  30. data/.claude/skills/memory-first-workflow +0 -1
  31. data/.claude/skills/quality-update/SKILL.md +0 -229
  32. data/.claude/skills/quality-update/implementation-guide.md +0 -346
  33. data/.claude/skills/release/SKILL.md +0 -206
  34. data/.claude/skills/review-commit/SKILL.md +0 -199
  35. data/.claude/skills/review-for-quality/SKILL.md +0 -154
  36. data/.claude/skills/review-for-quality/expert-checklists.md +0 -79
  37. data/.claude/skills/setup-memory +0 -1
  38. data/.claude/skills/study-repo/SKILL.md +0 -322
  39. data/.claude/skills/study-repo/analysis-template.md +0 -323
  40. data/.claude/skills/study-repo/focus-examples.md +0 -327
  41. data/.claude/skills/upgrade-dependencies/SKILL.md +0 -154
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '03389773428d20d899e3320e349cfd6a55e9c92df7b00837cf85466fe0741985'
4
- data.tar.gz: 14c91b7778987ffd1acbf11b5b58c4ccf0ed7eda6f187534ea27b4dcc987bf67
3
+ metadata.gz: fef30295649f5cdb709f3650bbdfe7411ddccd566a906eaa09247a1b88f895a5
4
+ data.tar.gz: 3edd0ef999cb0c4356ef83c43e20646eed8cc8b68b9a2c8c1963191935eaaf98
5
5
  SHA512:
6
- metadata.gz: b16bf521bfd7c496f4723a6029f981121ea2a76643eafb5045ab9528ab51543b47caf7d51474f21d69bb725e9544aa85c295ad303ee94cd7267f3d8cf4233184
7
- data.tar.gz: a3d8a65e03d9c62fb7d4e2feb1ec37b32c9513eb13fe3c7c6488922192a842e67ac237562b186278948cb4e8ef9372a8e5f2d63c45ab34e9a6ac4604102ff0d7
6
+ metadata.gz: e23ad823124ffec0b84d81d6f309dbc601ef5c475f827d21cfd4f9f9dbb7172a2c4832ded63c6f38bf9f46f060c5c4e04d94fe2bd9e2ed0044f7da58c38c70a8
7
+ data.tar.gz: d71ff604a9518affc8d153574dba34361b9b54b49fbffac228ff00c076ce05971b698782551a17bbfc072c84ce4f16e44680857671469db9d1d0c08e87cd9f94
@@ -7,7 +7,7 @@
7
7
  "plugins": [
8
8
  {
9
9
  "name": "claude-memory",
10
- "version": "0.13.1",
10
+ "version": "0.13.2",
11
11
  "source": "./",
12
12
  "description": "Long-term memory for Claude Code. Recalls architecture, conventions, and decisions across sessions, plus an episodic observation log of what happened — so Claude explains your codebase without file traversal, follows your patterns, learns from corrections, and never re-asks what it already learned.",
13
13
  "repository": "https://github.com/codenamev/claude_memory"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-memory",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Long-term memory for Claude Code. Recalls architecture, conventions, and decisions across sessions, plus an episodic observation log of what happened — so Claude explains your codebase without file traversal, follows your patterns, learns from corrections, and never re-asks what it already learned.",
5
5
  "author": {
6
6
  "name": "Valentino Stoll",
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.2] - 2026-06-27
8
+
9
+ Theme: **Robustness from a real bug report (issue #7) + a 28MB→606K gem.** Closes [#7](https://github.com/codenamev/claude_memory/issues/7) — three independent failure modes around embeddings, FTS, and process lifecycle — plus two backlog items surfaced alongside it. No schema changes, no breaking changes.
10
+
11
+ ### Fixed
12
+
13
+ - **Adopting a non-384-dim embedding model no longer hard-fails on an existing DB (#7 Finding 1).** The `facts_vec` vec0 table's width is immutable once created, and `embedding_dimensions` was only recorded *after* a successful index — so a tfidf/fresh DB silently created a 384 table and the first 768-dim insert raised `Expected 384 ... received 768`. `VectorIndex#recreate!` now drops + rebuilds `facts_vec` at the resolved width, and `IndexCommand#handle_dimension_mismatch` reads the table's actual width from its DDL and rebuilds up front (covering the unset-meta case, not just a recorded mismatch).
14
+ - **Corrupt FTS5 rank index is now detected and self-heals (#7 Finding 2 + #69).** Concurrent writers can leave `content_fts` so that plain `MATCH` works but `ORDER BY rank` raises "database disk image is malformed" — silently breaking recall while `PRAGMA integrity_check` passes. `doctor` now probes the rank path per DB (`Checks::FtsRankCheck`); the sweep (`PreCompact`/`SessionEnd`) auto-rebuilds on detection (`Sweep::Maintenance#repair_fts_rank`) so recall self-repairs without a manual `compact`; and `LexicalFTS#search` raises a `CorruptRankIndexError` with a compact hint instead of an unhandled stacktrace.
15
+ - **`serve-mcp` can no longer orphan (#7 Finding 3).** A hard kill of the client could leave the stdio MCP server blocked forever on `gets` (no stdin EOF), holding a SQLite connection — observed as serve-mcp processes lingering for days. A parent-death watchdog (`Server#orphaned?` + a 30s thread) exits the process once it's reparented away from its original parent.
16
+ - **The embedding provider no longer leaks into the test suite.** `setup-vectors` wrote `CLAUDE_MEMORY_EMBEDDING_PROVIDER` into the tracked `settings.json`, which Claude Code injects into every subprocess env — flipping provider-dependent specs. It now writes to `settings.local.json` (per-machine), and the suite clears `CLAUDE_MEMORY_EMBEDDING_*` for hermetic runs.
17
+
18
+ ### Changed
19
+
20
+ - **The published gem dropped from ~28MB to 606K (#71).** The gemspec's `git ls-files` manifest shipped the repo's own ~96MB dogfooding `.claude/memory.sqlite3` (and was trending toward RubyGems' 100MB ceiling). `.claude/` is now excluded from the manifest; users init their own DB, and the plugin manifest + commands/skills/output-styles (sourced at the top level) are unaffected. Regression-guarded by a manifest spec.
21
+
7
22
  ## [0.13.1] - 2026-06-23
8
23
 
9
24
  Theme: **The observational layer, audited and repaired.** A critical examination of every observation in a real (dogfooding) project DB found the episodic layer was producing ~no useful observations and injecting noise into sessions — three evidence-backed defects, now fixed (the data is in `docs/improvements.md` #72–#75). No schema changes, no breaking changes.
data/docs/improvements.md CHANGED
@@ -294,6 +294,8 @@ Source: 2026-04-28 1.0 readiness review (`docs/1_0_punchlist.md` #6)
294
294
 
295
295
  ### 69. Self-Heal the FTS Rank Index After Concurrent Ingest
296
296
 
297
+ **✅ Core shipped 2026-06-27** (detection + self-heal). **Proactive detection:** the doctor `Checks::FtsRankCheck` probes `MATCH … ORDER BY rank` per DB and flags corruption integrity_check misses (landed with issue #7 Finding 2, `2522044`). **Self-heal:** `Sweep::Maintenance#repair_fts_rank` probes the rank path each sweep (PreCompact/SessionEnd) and runs `LexicalFTS#rebuild!` on `malformed`, so recall self-repairs within the session that broke it — no manual `compact`. Also graceful: `LexicalFTS#search` now raises `CorruptRankIndexError` with a compact hint instead of a stacktrace. **Deferred:** the contention-reduction pragmas (busy_timeout + `BEGIN IMMEDIATE`) and the larger external-content-FTS5 redesign — they're risk/scope beyond the self-heal and not needed once the rank index auto-repairs.
298
+
297
299
  Source: 2026-06-16 live incident on `claude/observational-layer-design-7662r9` — observed first-hand, not a study.
298
300
 
299
301
  **Gap.** The contentless FTS5 index (`content_fts`) silently drifts into a broken state under concurrent writers: the ingest hook (`claude-memory hook ingest`) and the MCP server (`store_extraction` → `Index::LexicalFTS#index_content_item`) both write the same WAL DB, and a large ingest produces `"Database busy, retrying"` (`Store::RetryHandler`) followed by an FTS index where **plain `MATCH` works but `... ORDER BY rank` raises `database disk image is malformed`**. `integrity_check` passes and all rows are intact — so `recall`/`recall_index` ranking is silently degraded (the rank query throws or returns nothing) while nothing looks wrong. The only fix today is the user manually running `claude-memory compact` (which `rebuild_fts` + vacuums). Documented in `docs/influence/...` gotchas and surfaced reactively in the dashboard (`lib/claude_memory/dashboard/api.rb:338`), but never repaired automatically. Severe form (btree corruption, plain `MATCH` also failing) was separately seen when **two** memory MCP servers ran concurrently — de-duping to a single server removed that, but the benign rank-artifact still recurs from hook-vs-MCP contention alone.
@@ -506,6 +508,8 @@ Source: `docs/influence/mastra-observational-memory.md` — architecture study o
506
508
 
507
509
  ### 71. Exclude the project DB from the published gem (gem is 28MB, ~96MB of it the dogfooding DB)
508
510
 
511
+ **✅ Shipped 2026-06-27.** Added `.claude/` to the gemspec reject filter; the published gem dropped from **28MB → 606K**. The plugin manifest (`.claude-plugin/`) and top-level `commands/`/`skills/`/`output-styles/` are preserved (the plugin sources them there, not from `.claude/`); runtime `.claude/output_styles` references resolve against the *user's* project dir. Regression-guarded by `spec/claude_memory/gemspec_manifest_spec.rb`.
512
+
509
513
  Source: 2026-06-18 live observation while building the 0.13.0 release gem.
510
514
 
511
515
  **Problem.** `claude_memory.gemspec` builds its file list from `git ls-files` and rejects `bin/ Gemfile .gitignore .rspec spec/ .github/ .standard.yml` — but **not** `.claude/memory.sqlite3`, which is tracked (per the "always commit the project DB" convention). So the published gem *ships the repo's own dogfooding memory database*: the working-tree DB is ~96MB, compressing to a **28MB gem** (v0.6.0 was 280KB; the gem has been silently growing — 0.9.1 was 19MB — as the DB accumulates). Gem users get nothing from it (they init their own empty DB on install), it bloats every download, and it's trending toward RubyGems' 100MB ceiling.
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Commands
5
+ module Checks
6
+ # Probes the FTS5 BM25 ranking path (`MATCH ... ORDER BY rank`) — exactly
7
+ # what recall issues. A contentless FTS5 index can corrupt such that plain
8
+ # MATCH, `SELECT count(*)`, and `PRAGMA integrity_check` all pass but
9
+ # `ORDER BY rank` raises "database disk image is malformed", silently
10
+ # breaking recall while every other check reports healthy (issue #7,
11
+ # Finding 2). Recoverable with `claude-memory compact`.
12
+ class FtsRankCheck
13
+ PROBE_TERM = "the" # extremely common; matches any non-trivial English corpus
14
+
15
+ def initialize(db_path, label)
16
+ @db_path = db_path
17
+ @label = label
18
+ end
19
+
20
+ def call
21
+ return skip_result unless File.exist?(@db_path)
22
+
23
+ store = Store::SQLiteStore.new(@db_path)
24
+ begin
25
+ Index::LexicalFTS.new(store).search(PROBE_TERM, limit: 1)
26
+ ok_result
27
+ ensure
28
+ store.close
29
+ end
30
+ rescue Index::LexicalFTS::CorruptRankIndexError
31
+ corrupt_result
32
+ rescue => e
33
+ # Anything unrelated (e.g. no FTS table) is not this check's concern.
34
+ {status: :ok, label: fts_label, message: "#{@label} FTS5 rank probe skipped: #{e.message}", details: {}}
35
+ end
36
+
37
+ private
38
+
39
+ def fts_label = "#{@label}_fts"
40
+
41
+ def ok_result
42
+ {status: :ok, label: fts_label, message: "#{@label} FTS5 rank path: healthy", details: {}}
43
+ end
44
+
45
+ def corrupt_result
46
+ {
47
+ status: :error,
48
+ label: fts_label,
49
+ message: "#{@label} FTS5 rank index is corrupt — recall is broken (integrity_check misses this). Run 'claude-memory compact' to rebuild.",
50
+ details: {}
51
+ }
52
+ end
53
+
54
+ def skip_result
55
+ {status: :ok, label: fts_label, message: "#{@label} FTS5: no database", details: {}}
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -22,6 +22,8 @@ module ClaudeMemory
22
22
  Checks::DatabaseCheck.new(manager.project_db_path, "project"),
23
23
  Checks::DistillCheck.new(manager.global_db_path, "global"),
24
24
  Checks::DistillCheck.new(manager.project_db_path, "project"),
25
+ Checks::FtsRankCheck.new(manager.global_db_path, "global"),
26
+ Checks::FtsRankCheck.new(manager.project_db_path, "project"),
25
27
  Checks::VecCheck.new,
26
28
  Checks::EmbeddingsCheck.new,
27
29
  Checks::SnapshotCheck.new,
@@ -77,12 +77,30 @@ module ClaudeMemory
77
77
  run_indexing(store, facts, generator, tracker, operation_id, checkpoint, opts)
78
78
  end
79
79
 
80
+ # Reconcile the vec0 table's width with the resolved provider before
81
+ # indexing (issue #7, Finding 1). The vec0 width is immutable once the
82
+ # table is created and was only recorded in meta *after* a successful run,
83
+ # so an old tfidf/fresh DB silently created a 384 table and the first
84
+ # non-384 insert hard-failed. We detect the table's actual width directly
85
+ # (not via the meta, which may be unset) and rebuild when it differs.
80
86
  def handle_dimension_mismatch(store, generator, label)
81
- check = Embeddings::DimensionCheck.call(store, generator)
82
- return unless check.status == :mismatch
87
+ target = generator.dimensions
88
+ # Record the resolved dimension up front so a fresh table is created at
89
+ # the right width on first insert, not left at the 384 default.
90
+ store.set_meta("embedding_dimensions", target.to_s)
91
+ store.set_meta("embedding_provider", generator.name)
83
92
 
84
- stdout.puts "#{label.capitalize}: Embedding dimensions changed (#{check.stored} → #{check.current}), clearing stale embeddings..."
85
- clear_stale_embeddings(store)
93
+ vec_index = store.vector_index
94
+ return unless vec_index.available?
95
+
96
+ actual = vec_index.table_dimensions
97
+ return if actual == target # table already at the right width
98
+
99
+ if actual # genuine change: existing facts must re-embed at the new width
100
+ stdout.puts "#{label.capitalize}: Embedding dimensions changed (#{actual} → #{target}), rebuilding vector table..."
101
+ clear_stale_embeddings(store)
102
+ end
103
+ vec_index.recreate!(target)
86
104
  end
87
105
 
88
106
  def find_facts_to_index(store, tracker, label, opts)
@@ -14,8 +14,8 @@ module ClaudeMemory
14
14
  # 1. Verify the chosen provider is loadable. For fastembed, surface
15
15
  # a clear install command if the gem isn't on $LOAD_PATH.
16
16
  # 2. Persist CLAUDE_MEMORY_EMBEDDING_PROVIDER (and optional model)
17
- # into the project's .claude/settings.json env block, the same
18
- # mechanism Claude Code uses for OTel env (see OTel::SettingsWriter).
17
+ # into the project's .claude/settings.local.json env block (per-machine,
18
+ # gitignored the provider is a developer choice, not a repo default).
19
19
  # 3. Re-embed existing facts under the new provider (unless --no-reindex).
20
20
  # 4. Report the final state — provider, dimensions, stored alignment.
21
21
  class SetupVectorsCommand < BaseCommand
@@ -110,8 +110,14 @@ module ClaudeMemory
110
110
  false
111
111
  end
112
112
 
113
+ # Write to settings.local.json (per-machine, gitignored), NOT the tracked
114
+ # settings.json: the embedding provider is a developer choice. Committing
115
+ # it would force the optional fastembed dependency on collaborators and —
116
+ # because Claude Code injects settings env into every subprocess — leak a
117
+ # non-default provider into the test suite, flipping provider-dependent
118
+ # specs (issue #7 follow-up, 2026-06-25).
113
119
  def settings_path
114
- File.join(claude_dir, "settings.json")
120
+ File.join(claude_dir, "settings.local.json")
115
121
  end
116
122
 
117
123
  def claude_dir
@@ -3,6 +3,15 @@
3
3
  module ClaudeMemory
4
4
  module Index
5
5
  class LexicalFTS
6
+ # Raised when the FTS5 BM25 ranking path fails as "malformed" while the
7
+ # rest of the DB is fine — a corrupt contentless FTS5 index that
8
+ # PRAGMA integrity_check misses (issue #7, Finding 2). Recoverable with
9
+ # `claude-memory compact` (which runs #rebuild!).
10
+ class CorruptRankIndexError < StandardError; end
11
+
12
+ RANK_CORRUPTION_HINT = "FTS5 rank index is corrupt — recall is broken even though " \
13
+ "the database otherwise looks healthy. Run `claude-memory compact` to rebuild it."
14
+
6
15
  def initialize(store)
7
16
  @store = store
8
17
  @db = store.db
@@ -35,17 +44,19 @@ module ClaudeMemory
35
44
  end
36
45
 
37
46
  escaped_query = escape_fts_query(query)
38
- if contentless?
39
- @db.fetch(
40
- "SELECT rowid AS content_item_id FROM content_fts WHERE text MATCH ? ORDER BY rank LIMIT ?",
41
- escaped_query, limit
42
- ).map { |row| row[:content_item_id] }
43
- else
44
- @db[:content_fts]
45
- .where(Sequel.lit("text MATCH ?", escaped_query))
46
- .order(:rank)
47
- .limit(limit)
48
- .select_map(:content_item_id)
47
+ with_rank_index do
48
+ if contentless?
49
+ @db.fetch(
50
+ "SELECT rowid AS content_item_id FROM content_fts WHERE text MATCH ? ORDER BY rank LIMIT ?",
51
+ escaped_query, limit
52
+ ).map { |row| row[:content_item_id] }
53
+ else
54
+ @db[:content_fts]
55
+ .where(Sequel.lit("text MATCH ?", escaped_query))
56
+ .order(:rank)
57
+ .limit(limit)
58
+ .select_map(:content_item_id)
59
+ end
49
60
  end
50
61
  end
51
62
 
@@ -59,18 +70,20 @@ module ClaudeMemory
59
70
  return [] if query.strip == "*"
60
71
 
61
72
  escaped_query = escape_fts_query(query)
62
- if contentless?
63
- @db.fetch(
64
- "SELECT rowid AS content_item_id, rank FROM content_fts WHERE text MATCH ? ORDER BY rank LIMIT ?",
65
- escaped_query, limit
66
- ).all
67
- else
68
- @db[:content_fts]
69
- .where(Sequel.lit("text MATCH ?", escaped_query))
70
- .order(:rank)
71
- .limit(limit)
72
- .select(Sequel.lit("content_item_id, rank"))
73
- .all
73
+ with_rank_index do
74
+ if contentless?
75
+ @db.fetch(
76
+ "SELECT rowid AS content_item_id, rank FROM content_fts WHERE text MATCH ? ORDER BY rank LIMIT ?",
77
+ escaped_query, limit
78
+ ).all
79
+ else
80
+ @db[:content_fts]
81
+ .where(Sequel.lit("text MATCH ?", escaped_query))
82
+ .order(:rank)
83
+ .limit(limit)
84
+ .select(Sequel.lit("content_item_id, rank"))
85
+ .all
86
+ end
74
87
  end
75
88
  end
76
89
 
@@ -117,6 +130,17 @@ module ClaudeMemory
117
130
 
118
131
  private
119
132
 
133
+ # Run a `MATCH ... ORDER BY rank` query, translating the narrow
134
+ # "malformed"-on-rank failure into an actionable error instead of an
135
+ # unhandled Extralite stacktrace (issue #7, Finding 2). Any other error
136
+ # propagates unchanged.
137
+ def with_rank_index
138
+ yield
139
+ rescue => e
140
+ raise unless e.message.to_s.include?("malformed")
141
+ raise CorruptRankIndexError, RANK_CORRUPTION_HINT
142
+ end
143
+
120
144
  def contentless?
121
145
  @contentless
122
146
  end
@@ -134,6 +134,33 @@ module ClaudeMemory
134
134
  true
135
135
  end
136
136
 
137
+ # Drop and rebuild facts_vec at `dimensions`. A vec0 column width is
138
+ # immutable once the table is created, so adopting a model of a different
139
+ # dimension (or any model on a DB whose table was created at the 384
140
+ # default) requires a full rebuild — clearing rows isn't enough (issue #7,
141
+ # Finding 1). Requires the sqlite-vec extension loaded so the vec0
142
+ # destructor runs on DROP.
143
+ # @param dimensions [Integer] new embedding width
144
+ def recreate!(dimensions)
145
+ return false unless available?
146
+
147
+ @dimensions = dimensions
148
+ @db.run("DROP TABLE IF EXISTS facts_vec")
149
+ @vec_table_ensured = false
150
+ ensure_vec_table!
151
+ true
152
+ end
153
+
154
+ # The width facts_vec was actually created with, parsed from its DDL — or
155
+ # nil when the table doesn't exist yet. Detects a stale-width table even
156
+ # when the embedding_dimensions meta was never written (old tfidf DBs),
157
+ # which is exactly the case that silently left a 384 table in place.
158
+ # @return [Integer, nil]
159
+ def table_dimensions
160
+ ddl = @db[:sqlite_master].where(type: "table", name: "facts_vec").get(:sql)
161
+ ddl && ddl[/embedding\s+float\[(\d+)\]/, 1]&.to_i
162
+ end
163
+
137
164
  # Number of entries in the vec0 virtual table
138
165
  def count
139
166
  return 0 unless available?
@@ -14,16 +14,19 @@ module ClaudeMemory
14
14
  # and writes JSON responses to output.
15
15
  class Server
16
16
  PROTOCOL_VERSION = "2024-11-05"
17
+ ORPHAN_CHECK_INTERVAL_SECONDS = 30
17
18
 
18
19
  # @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database backend
19
20
  # @param input [IO] input stream for JSON-RPC requests (default: $stdin)
20
21
  # @param output [IO] output stream for JSON-RPC responses (default: $stdout)
21
- def initialize(store_or_manager, input: $stdin, output: $stdout)
22
+ # @param parent_pid [Integer] pid to watch for orphaning (default: caller's parent)
23
+ def initialize(store_or_manager, input: $stdin, output: $stdout, parent_pid: Process.ppid)
22
24
  @store_or_manager = store_or_manager
23
25
  @tools = Tools.new(store_or_manager)
24
26
  @telemetry = Telemetry.new(store_or_manager)
25
27
  @input = input
26
28
  @output = output
29
+ @parent_pid = parent_pid
27
30
  @running = false
28
31
  end
29
32
 
@@ -31,12 +34,24 @@ module ClaudeMemory
31
34
  # @return [void]
32
35
  def run
33
36
  @running = true
37
+ watchdog = start_orphan_watchdog
34
38
  while @running
35
39
  line = @input.gets
36
40
  break unless line
37
41
 
38
42
  handle_message(line.strip)
39
43
  end
44
+ ensure
45
+ watchdog&.kill
46
+ end
47
+
48
+ # True once our original parent has exited and we've been reparented
49
+ # (to PID 1 / a subreaper). Claude Code spawns one stdio MCP server per
50
+ # session; a hard kill of the client can leave the server blocked on
51
+ # `gets` forever, holding a SQLite connection — the orphan leak in issue
52
+ # #7, Finding 3. The watchdog uses this to terminate.
53
+ def orphaned?
54
+ @parent_pid > 1 && Process.ppid != @parent_pid
40
55
  end
41
56
 
42
57
  # Signal the read loop to exit after the current message.
@@ -47,6 +62,23 @@ module ClaudeMemory
47
62
 
48
63
  private
49
64
 
65
+ # Background watch that terminates the process when the parent dies, since
66
+ # the read loop is otherwise blocked indefinitely on `gets`. Returns the
67
+ # Thread, or nil when there's no meaningful parent to watch (e.g. a test
68
+ # harness or a manually-launched server). Uses `exit!` because the orphaned
69
+ # parent is gone — there's nothing reading our output to flush to, and the
70
+ # OS releases the SQLite connection on process exit.
71
+ def start_orphan_watchdog
72
+ return if @parent_pid <= 1
73
+
74
+ Thread.new do
75
+ loop do
76
+ sleep ORPHAN_CHECK_INTERVAL_SECONDS
77
+ exit!(0) if orphaned?
78
+ end
79
+ end
80
+ end
81
+
50
82
  # @return [void]
51
83
  def handle_message(line)
52
84
  return if line.empty?
@@ -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.1"
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"
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.1
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
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