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.
- checksums.yaml +4 -4
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -1
- data/CHANGELOG.md +15 -0
- data/docs/improvements.md +4 -0
- data/lib/claude_memory/commands/checks/fts_rank_check.rb +60 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/index_command.rb +22 -4
- data/lib/claude_memory/commands/setup_vectors_command.rb +9 -3
- data/lib/claude_memory/index/lexical_fts.rb +47 -23
- data/lib/claude_memory/index/vector_index.rb +27 -0
- data/lib/claude_memory/mcp/server.rb +33 -1
- data/lib/claude_memory/sweep/maintenance.rb +22 -0
- data/lib/claude_memory/sweep/sweeper.rb +1 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +1 -0
- metadata +2 -25
- data/.claude/CLAUDE.md +0 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/output-styles/memory-aware.md +0 -1
- data/.claude/rules/claude_memory.generated.md +0 -87
- data/.claude/settings.json +0 -113
- data/.claude/settings.local.json +0 -59
- data/.claude/skills/check-memory/DEPRECATED.md +0 -29
- data/.claude/skills/check-memory/SKILL.md +0 -87
- data/.claude/skills/dashboard/SKILL.md +0 -42
- data/.claude/skills/debug-memory +0 -1
- data/.claude/skills/improve/SKILL.md +0 -631
- data/.claude/skills/improve/feature-patterns.md +0 -1221
- data/.claude/skills/memory-first-workflow +0 -1
- data/.claude/skills/quality-update/SKILL.md +0 -229
- data/.claude/skills/quality-update/implementation-guide.md +0 -346
- data/.claude/skills/release/SKILL.md +0 -206
- data/.claude/skills/review-commit/SKILL.md +0 -199
- data/.claude/skills/review-for-quality/SKILL.md +0 -154
- data/.claude/skills/review-for-quality/expert-checklists.md +0 -79
- data/.claude/skills/setup-memory +0 -1
- data/.claude/skills/study-repo/SKILL.md +0 -322
- data/.claude/skills/study-repo/analysis-template.md +0 -323
- data/.claude/skills/study-repo/focus-examples.md +0 -327
- data/.claude/skills/upgrade-dependencies/SKILL.md +0 -154
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fef30295649f5cdb709f3650bbdfe7411ddccd566a906eaa09247a1b88f895a5
|
|
4
|
+
data.tar.gz: 3edd0ef999cb0c4356ef83c43e20646eed8cc8b68b9a2c8c1963191935eaaf98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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"
|
data/.claude-plugin/plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-memory",
|
|
3
|
-
"version": "0.13.
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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,
|
|
18
|
-
#
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
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.
|
|
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
data/.claude/memory.sqlite3
DELETED
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
../../output-styles/memory-aware.md
|