claude_memory 0.9.1 → 0.11.0
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/memory.sqlite3 +0 -0
- data/.claude/skills/dashboard/SKILL.md +42 -0
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -1
- data/CHANGELOG.md +130 -0
- data/CLAUDE.md +30 -6
- data/README.md +66 -2
- data/db/migrations/015_add_activity_events.rb +26 -0
- data/db/migrations/016_add_moment_feedback.rb +22 -0
- data/db/migrations/017_add_last_recalled_at.rb +15 -0
- data/docs/1_0_punchlist.md +371 -0
- data/docs/EXAMPLES.md +41 -2
- data/docs/GETTING_STARTED.md +33 -4
- data/docs/architecture.md +22 -7
- data/docs/audit-queries.md +131 -0
- data/docs/dashboard.md +192 -0
- data/docs/improvements.md +650 -9
- data/docs/influence/cq.md +187 -0
- data/docs/plugin.md +13 -6
- data/docs/quality_review.md +524 -172
- data/docs/reflection_memory_as_accumulating_judgment.md +67 -0
- data/lib/claude_memory/activity_log.rb +86 -0
- data/lib/claude_memory/commands/census_command.rb +210 -0
- data/lib/claude_memory/commands/completion_command.rb +3 -0
- data/lib/claude_memory/commands/dashboard_command.rb +54 -0
- data/lib/claude_memory/commands/dedupe_conflicts_command.rb +55 -0
- data/lib/claude_memory/commands/digest_command.rb +273 -0
- data/lib/claude_memory/commands/hook_command.rb +61 -2
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +7 -4
- data/lib/claude_memory/commands/reclassify_references_command.rb +56 -0
- data/lib/claude_memory/commands/registry.rb +7 -1
- data/lib/claude_memory/commands/show_command.rb +90 -0
- data/lib/claude_memory/commands/skills/distill-transcripts.md +13 -1
- data/lib/claude_memory/commands/stats_command.rb +131 -2
- data/lib/claude_memory/commands/sweep_command.rb +2 -0
- data/lib/claude_memory/configuration.rb +16 -0
- data/lib/claude_memory/core/relative_time.rb +9 -0
- data/lib/claude_memory/dashboard/api.rb +610 -0
- data/lib/claude_memory/dashboard/conflicts.rb +279 -0
- data/lib/claude_memory/dashboard/efficacy.rb +127 -0
- data/lib/claude_memory/dashboard/fact_presenter.rb +109 -0
- data/lib/claude_memory/dashboard/health.rb +175 -0
- data/lib/claude_memory/dashboard/index.html +2707 -0
- data/lib/claude_memory/dashboard/knowledge.rb +136 -0
- data/lib/claude_memory/dashboard/moments.rb +244 -0
- data/lib/claude_memory/dashboard/reuse.rb +97 -0
- data/lib/claude_memory/dashboard/scoped_fact_resolver.rb +95 -0
- data/lib/claude_memory/dashboard/server.rb +211 -0
- data/lib/claude_memory/dashboard/timeline.rb +68 -0
- data/lib/claude_memory/dashboard/trust.rb +454 -0
- data/lib/claude_memory/distill/bare_conclusion_detector.rb +71 -0
- data/lib/claude_memory/distill/reference_material_detector.rb +78 -0
- data/lib/claude_memory/hook/auto_memory_mirror.rb +112 -0
- data/lib/claude_memory/hook/context_injector.rb +97 -3
- data/lib/claude_memory/hook/handler.rb +191 -3
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +8 -0
- data/lib/claude_memory/mcp/query_guide.rb +11 -0
- data/lib/claude_memory/mcp/text_summary.rb +29 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +13 -0
- data/lib/claude_memory/mcp/tools.rb +148 -0
- data/lib/claude_memory/publish.rb +13 -21
- data/lib/claude_memory/recall/stale_detector.rb +67 -0
- data/lib/claude_memory/resolve/predicate_policy.rb +2 -0
- data/lib/claude_memory/resolve/resolver.rb +41 -11
- data/lib/claude_memory/store/llm_cache.rb +68 -0
- data/lib/claude_memory/store/metrics_aggregator.rb +96 -0
- data/lib/claude_memory/store/schema_manager.rb +1 -1
- data/lib/claude_memory/store/sqlite_store.rb +47 -143
- data/lib/claude_memory/store/store_manager.rb +29 -0
- data/lib/claude_memory/sweep/maintenance.rb +216 -0
- data/lib/claude_memory/sweep/recall_timestamp_refresher.rb +83 -0
- data/lib/claude_memory/sweep/sweeper.rb +2 -0
- data/lib/claude_memory/templates/hooks.example.json +5 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +24 -0
- metadata +51 -1
|
@@ -13,13 +13,16 @@ module ClaudeMemory
|
|
|
13
13
|
SCOPE_PROJECT = "project"
|
|
14
14
|
|
|
15
15
|
def call(args)
|
|
16
|
-
opts = parse_options(args, {scope: SCOPE_ALL, tools: false, since_days: nil}) do |o|
|
|
16
|
+
opts = parse_options(args, {scope: SCOPE_ALL, tools: false, tokens: false, stale: false, since_days: nil, stale_days: nil}) do |o|
|
|
17
17
|
OptionParser.new do |parser|
|
|
18
18
|
parser.banner = "Usage: claude-memory stats [options]"
|
|
19
19
|
parser.on("--scope SCOPE", ["all", "global", "project"],
|
|
20
20
|
"Show stats for: all (default), global, or project") { |v| o[:scope] = v }
|
|
21
21
|
parser.on("--tools", "Show MCP tool-call usage stats") { o[:tools] = true }
|
|
22
|
-
parser.on("--
|
|
22
|
+
parser.on("--tokens", "Show SessionStart context-injection token budget") { o[:tokens] = true }
|
|
23
|
+
parser.on("--stale", "Show facts not recalled in CLAUDE_MEMORY_STALE_DAYS (default 14)") { o[:stale] = true }
|
|
24
|
+
parser.on("--since DAYS", Integer, "Limit --tools/--tokens to last N days") { |v| o[:since_days] = v }
|
|
25
|
+
parser.on("--stale-days N", Integer, "Override staleness threshold for --stale") { |v| o[:stale_days] = v }
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
return 1 if opts.nil?
|
|
@@ -28,6 +31,14 @@ module ClaudeMemory
|
|
|
28
31
|
return print_mcp_tool_call_stats(opts[:since_days])
|
|
29
32
|
end
|
|
30
33
|
|
|
34
|
+
if opts[:tokens]
|
|
35
|
+
return print_token_budget_stats(opts[:since_days])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if opts[:stale]
|
|
39
|
+
return print_stale_facts(opts[:stale_days])
|
|
40
|
+
end
|
|
41
|
+
|
|
31
42
|
manager = ClaudeMemory::Store::StoreManager.new
|
|
32
43
|
|
|
33
44
|
stdout.puts "ClaudeMemory Statistics"
|
|
@@ -48,6 +59,37 @@ module ClaudeMemory
|
|
|
48
59
|
|
|
49
60
|
private
|
|
50
61
|
|
|
62
|
+
def print_stale_facts(override_days)
|
|
63
|
+
threshold = override_days || ClaudeMemory::Configuration.new.stale_days
|
|
64
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
65
|
+
result = ClaudeMemory::Recall::StaleDetector.stale_facts(manager, threshold_days: threshold)
|
|
66
|
+
|
|
67
|
+
stdout.puts "Stale facts (last_recalled_at older than #{threshold} day#{"s" unless threshold == 1})"
|
|
68
|
+
stdout.puts "=" * 60
|
|
69
|
+
|
|
70
|
+
if result[:total].zero?
|
|
71
|
+
stdout.puts "No stale facts."
|
|
72
|
+
stdout.puts ""
|
|
73
|
+
stdout.puts "Run `claude-memory sweep` to refresh last_recalled_at from activity_events."
|
|
74
|
+
else
|
|
75
|
+
stdout.puts "Total: #{result[:total]} (project=#{result[:project].size}, global=#{result[:global].size})"
|
|
76
|
+
stdout.puts ""
|
|
77
|
+
%i[project global].each do |scope|
|
|
78
|
+
rows = result[scope]
|
|
79
|
+
next if rows.empty?
|
|
80
|
+
stdout.puts "## #{scope.to_s.upcase}"
|
|
81
|
+
rows.each do |row|
|
|
82
|
+
last = row[:last_recalled_at] || "never"
|
|
83
|
+
stdout.puts " ##{row[:id]} [#{row[:predicate]}] #{row[:object_literal]&.slice(0, 80)} (last: #{last})"
|
|
84
|
+
end
|
|
85
|
+
stdout.puts ""
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
manager.close
|
|
90
|
+
0
|
|
91
|
+
end
|
|
92
|
+
|
|
51
93
|
def open_readonly(db_path)
|
|
52
94
|
Sequel.connect("extralite://#{db_path}")
|
|
53
95
|
end
|
|
@@ -312,6 +354,93 @@ module ClaudeMemory
|
|
|
312
354
|
1
|
|
313
355
|
end
|
|
314
356
|
|
|
357
|
+
TOKEN_BUCKETS = [
|
|
358
|
+
["<500", 0, 500],
|
|
359
|
+
["500-1000", 500, 1000],
|
|
360
|
+
["1000-2000", 1000, 2000],
|
|
361
|
+
["2000-5000", 2000, 5000],
|
|
362
|
+
["5000+", 5000, Float::INFINITY]
|
|
363
|
+
].freeze
|
|
364
|
+
|
|
365
|
+
def print_token_budget_stats(since_days)
|
|
366
|
+
manager = ClaudeMemory::Store::StoreManager.new
|
|
367
|
+
db_path = manager.project_db_path
|
|
368
|
+
|
|
369
|
+
stdout.puts "SessionStart Context Token Budget"
|
|
370
|
+
stdout.puts "=" * 50
|
|
371
|
+
|
|
372
|
+
unless File.exist?(db_path)
|
|
373
|
+
stdout.puts "Project database does not exist: #{db_path}"
|
|
374
|
+
manager.close
|
|
375
|
+
return 0
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
db = open_readonly(db_path)
|
|
379
|
+
|
|
380
|
+
unless db.table_exists?(:activity_events)
|
|
381
|
+
stdout.puts "No activity telemetry recorded yet."
|
|
382
|
+
db.disconnect
|
|
383
|
+
manager.close
|
|
384
|
+
return 0
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
dataset = db[:activity_events]
|
|
388
|
+
.where(event_type: "hook_context", status: "success")
|
|
389
|
+
if since_days
|
|
390
|
+
cutoff = (Time.now - since_days * 86400).utc.iso8601
|
|
391
|
+
dataset = dataset.where { occurred_at >= cutoff }
|
|
392
|
+
stdout.puts "Window: last #{since_days} day#{"s" unless since_days == 1}"
|
|
393
|
+
else
|
|
394
|
+
stdout.puts "Window: all time"
|
|
395
|
+
end
|
|
396
|
+
stdout.puts
|
|
397
|
+
|
|
398
|
+
tokens = dataset.select_map(:detail_json).filter_map do |json|
|
|
399
|
+
next unless json
|
|
400
|
+
value = JSON.parse(json)["context_tokens"]
|
|
401
|
+
value if value.is_a?(Integer) && value > 0
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
if tokens.empty?
|
|
405
|
+
stdout.puts "No context injections recorded in window."
|
|
406
|
+
stdout.puts ""
|
|
407
|
+
stdout.puts "Token telemetry is recorded automatically on SessionStart hooks."
|
|
408
|
+
stdout.puts "Run a Claude Code session in this project to populate."
|
|
409
|
+
db.disconnect
|
|
410
|
+
manager.close
|
|
411
|
+
return 0
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
sorted = tokens.sort
|
|
415
|
+
total = sorted.size
|
|
416
|
+
stdout.puts "Sessions: #{format_number(total)}"
|
|
417
|
+
stdout.puts "p50: #{format_number(percentile(sorted, 0.50))} tokens"
|
|
418
|
+
stdout.puts "p95: #{format_number(percentile(sorted, 0.95))} tokens"
|
|
419
|
+
stdout.puts "Avg: #{format_number((sorted.sum.to_f / total).round)} tokens"
|
|
420
|
+
stdout.puts "Min: #{format_number(sorted.first)} tokens"
|
|
421
|
+
stdout.puts "Max: #{format_number(sorted.last)} tokens"
|
|
422
|
+
stdout.puts ""
|
|
423
|
+
print_token_distribution(sorted)
|
|
424
|
+
|
|
425
|
+
db.disconnect
|
|
426
|
+
manager.close
|
|
427
|
+
0
|
|
428
|
+
rescue Sequel::DatabaseError, JSON::ParserError, Extralite::Error => e
|
|
429
|
+
stderr.puts "Error reading token telemetry: #{e.message}"
|
|
430
|
+
1
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def print_token_distribution(sorted)
|
|
434
|
+
total = sorted.size
|
|
435
|
+
stdout.puts "Distribution:"
|
|
436
|
+
TOKEN_BUCKETS.each do |label, low, high|
|
|
437
|
+
count = sorted.count { |t| t >= low && t < high }
|
|
438
|
+
pct = (count * 100.0 / total).round(1)
|
|
439
|
+
bar = "█" * (pct / 5).round
|
|
440
|
+
stdout.puts " #{label.ljust(12)} #{count.to_s.rjust(5)} (#{pct.to_s.rjust(5)}%) #{bar}"
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
315
444
|
def print_per_tool_breakdown(dataset)
|
|
316
445
|
stdout.puts "Per-tool breakdown:"
|
|
317
446
|
stdout.puts " #{"Tool".ljust(28)} #{"Calls".rjust(7)} #{"Avg ms".rjust(8)} #{"P95 ms".rjust(8)} #{"Err %".rjust(6)}"
|
|
@@ -19,12 +19,14 @@ module ClaudeMemory
|
|
|
19
19
|
|
|
20
20
|
stdout.puts "Running sweep on #{opts[:scope]} database with #{opts[:budget]}s budget..."
|
|
21
21
|
stats = sweeper.run!(budget_seconds: opts[:budget])
|
|
22
|
+
refresh_counts = ClaudeMemory::Sweep::RecallTimestampRefresher.new(manager).refresh!
|
|
22
23
|
|
|
23
24
|
stdout.puts "Sweep complete:"
|
|
24
25
|
stdout.puts " Proposed facts expired: #{stats[:proposed_facts_expired]}"
|
|
25
26
|
stdout.puts " Disputed facts expired: #{stats[:disputed_facts_expired]}"
|
|
26
27
|
stdout.puts " Orphaned provenance deleted: #{stats[:orphaned_provenance_deleted]}"
|
|
27
28
|
stdout.puts " Old content pruned: #{stats[:old_content_pruned]}"
|
|
29
|
+
stdout.puts " Recall timestamps refreshed: project=#{refresh_counts[:project]}, global=#{refresh_counts[:global]}"
|
|
28
30
|
stdout.puts " Elapsed: #{stats[:elapsed_seconds].round(2)}s"
|
|
29
31
|
stdout.puts " Budget honored: #{stats[:budget_honored]}"
|
|
30
32
|
|
|
@@ -50,6 +50,22 @@ module ClaudeMemory
|
|
|
50
50
|
env["CLAUDE_TRANSCRIPT_PATH"]
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Default staleness threshold (in days) for #35 access-based staleness.
|
|
54
|
+
# Active facts whose last_recalled_at is older than this — or never set,
|
|
55
|
+
# for facts created earlier than the same window — are flagged as
|
|
56
|
+
# candidates for review. Override via CLAUDE_MEMORY_STALE_DAYS.
|
|
57
|
+
DEFAULT_STALE_DAYS = 14
|
|
58
|
+
|
|
59
|
+
# @return [Integer] staleness threshold in days
|
|
60
|
+
def stale_days
|
|
61
|
+
raw = env["CLAUDE_MEMORY_STALE_DAYS"]
|
|
62
|
+
return DEFAULT_STALE_DAYS if raw.nil? || raw.empty?
|
|
63
|
+
parsed = Integer(raw, 10)
|
|
64
|
+
(parsed > 0) ? parsed : DEFAULT_STALE_DAYS
|
|
65
|
+
rescue ArgumentError
|
|
66
|
+
DEFAULT_STALE_DAYS
|
|
67
|
+
end
|
|
68
|
+
|
|
53
69
|
private
|
|
54
70
|
|
|
55
71
|
def resolve_project_dir
|
|
@@ -37,6 +37,15 @@ module ClaudeMemory
|
|
|
37
37
|
nil
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Parse a timestamp value into a Unix epoch integer; returns 0 when the
|
|
41
|
+
# value is unparseable. Used by sort comparators that need a stable
|
|
42
|
+
# numeric key without an exception path.
|
|
43
|
+
def self.to_epoch(value)
|
|
44
|
+
Time.parse(value.to_s).to_i
|
|
45
|
+
rescue ArgumentError, TypeError
|
|
46
|
+
0
|
|
47
|
+
end
|
|
48
|
+
|
|
40
49
|
def self.format_absolute(time)
|
|
41
50
|
time.strftime("%Y-%m-%d")
|
|
42
51
|
end
|