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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/skills/dashboard/SKILL.md +42 -0
  4. data/.claude-plugin/marketplace.json +1 -1
  5. data/.claude-plugin/plugin.json +1 -1
  6. data/CHANGELOG.md +130 -0
  7. data/CLAUDE.md +30 -6
  8. data/README.md +66 -2
  9. data/db/migrations/015_add_activity_events.rb +26 -0
  10. data/db/migrations/016_add_moment_feedback.rb +22 -0
  11. data/db/migrations/017_add_last_recalled_at.rb +15 -0
  12. data/docs/1_0_punchlist.md +371 -0
  13. data/docs/EXAMPLES.md +41 -2
  14. data/docs/GETTING_STARTED.md +33 -4
  15. data/docs/architecture.md +22 -7
  16. data/docs/audit-queries.md +131 -0
  17. data/docs/dashboard.md +192 -0
  18. data/docs/improvements.md +650 -9
  19. data/docs/influence/cq.md +187 -0
  20. data/docs/plugin.md +13 -6
  21. data/docs/quality_review.md +524 -172
  22. data/docs/reflection_memory_as_accumulating_judgment.md +67 -0
  23. data/lib/claude_memory/activity_log.rb +86 -0
  24. data/lib/claude_memory/commands/census_command.rb +210 -0
  25. data/lib/claude_memory/commands/completion_command.rb +3 -0
  26. data/lib/claude_memory/commands/dashboard_command.rb +54 -0
  27. data/lib/claude_memory/commands/dedupe_conflicts_command.rb +55 -0
  28. data/lib/claude_memory/commands/digest_command.rb +273 -0
  29. data/lib/claude_memory/commands/hook_command.rb +61 -2
  30. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +7 -4
  31. data/lib/claude_memory/commands/reclassify_references_command.rb +56 -0
  32. data/lib/claude_memory/commands/registry.rb +7 -1
  33. data/lib/claude_memory/commands/show_command.rb +90 -0
  34. data/lib/claude_memory/commands/skills/distill-transcripts.md +13 -1
  35. data/lib/claude_memory/commands/stats_command.rb +131 -2
  36. data/lib/claude_memory/commands/sweep_command.rb +2 -0
  37. data/lib/claude_memory/configuration.rb +16 -0
  38. data/lib/claude_memory/core/relative_time.rb +9 -0
  39. data/lib/claude_memory/dashboard/api.rb +610 -0
  40. data/lib/claude_memory/dashboard/conflicts.rb +279 -0
  41. data/lib/claude_memory/dashboard/efficacy.rb +127 -0
  42. data/lib/claude_memory/dashboard/fact_presenter.rb +109 -0
  43. data/lib/claude_memory/dashboard/health.rb +175 -0
  44. data/lib/claude_memory/dashboard/index.html +2707 -0
  45. data/lib/claude_memory/dashboard/knowledge.rb +136 -0
  46. data/lib/claude_memory/dashboard/moments.rb +244 -0
  47. data/lib/claude_memory/dashboard/reuse.rb +97 -0
  48. data/lib/claude_memory/dashboard/scoped_fact_resolver.rb +95 -0
  49. data/lib/claude_memory/dashboard/server.rb +211 -0
  50. data/lib/claude_memory/dashboard/timeline.rb +68 -0
  51. data/lib/claude_memory/dashboard/trust.rb +454 -0
  52. data/lib/claude_memory/distill/bare_conclusion_detector.rb +71 -0
  53. data/lib/claude_memory/distill/reference_material_detector.rb +78 -0
  54. data/lib/claude_memory/hook/auto_memory_mirror.rb +112 -0
  55. data/lib/claude_memory/hook/context_injector.rb +97 -3
  56. data/lib/claude_memory/hook/handler.rb +191 -3
  57. data/lib/claude_memory/mcp/handlers/management_handlers.rb +8 -0
  58. data/lib/claude_memory/mcp/query_guide.rb +11 -0
  59. data/lib/claude_memory/mcp/text_summary.rb +29 -0
  60. data/lib/claude_memory/mcp/tool_definitions.rb +13 -0
  61. data/lib/claude_memory/mcp/tools.rb +148 -0
  62. data/lib/claude_memory/publish.rb +13 -21
  63. data/lib/claude_memory/recall/stale_detector.rb +67 -0
  64. data/lib/claude_memory/resolve/predicate_policy.rb +2 -0
  65. data/lib/claude_memory/resolve/resolver.rb +41 -11
  66. data/lib/claude_memory/store/llm_cache.rb +68 -0
  67. data/lib/claude_memory/store/metrics_aggregator.rb +96 -0
  68. data/lib/claude_memory/store/schema_manager.rb +1 -1
  69. data/lib/claude_memory/store/sqlite_store.rb +47 -143
  70. data/lib/claude_memory/store/store_manager.rb +29 -0
  71. data/lib/claude_memory/sweep/maintenance.rb +216 -0
  72. data/lib/claude_memory/sweep/recall_timestamp_refresher.rb +83 -0
  73. data/lib/claude_memory/sweep/sweeper.rb +2 -0
  74. data/lib/claude_memory/templates/hooks.example.json +5 -0
  75. data/lib/claude_memory/version.rb +1 -1
  76. data/lib/claude_memory.rb +24 -0
  77. 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("--since DAYS", Integer, "Limit --tools to last N days") { |v| o[:since_days] = v }
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