Class: ClaudeMemory::Hook::AutoMemoryMirror

Inherits:
Object
  • Object
show all
Defined in:
lib/claude_memory/hook/auto_memory_mirror.rb

Overview

Mirrors Claude Code auto-memory (~/.claude/projects/<slug>/memory/*.md) into extraction candidates surfaced alongside the SessionStart distillation prompt. Diffs files against a per-project state file (mtime+md5) so only new or changed entries are emitted. Idempotent — unchanged files are skipped on re-run.

The emission is a hint to Claude that auto-memory has content worth mirroring into claude_memory via ‘memory.store_extraction`. The mirror never writes facts itself; the normal extraction review flow still applies.

Constant Summary collapse

MAX_CANDIDATES =
5
MAX_TEXT_PER_ITEM =
1500
STATE_FILENAME =
"auto_memory_mirror.json"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(auto_memory_dir:, state_file:) ⇒ AutoMemoryMirror

Returns a new instance of AutoMemoryMirror.



35
36
37
38
# File 'lib/claude_memory/hook/auto_memory_mirror.rb', line 35

def initialize(auto_memory_dir:, state_file:)
  @auto_memory_dir = auto_memory_dir
  @state_file = state_file
end

Class Method Details

.default_dir(project_path, claude_config_dir) ⇒ Object

Derive auto-memory directory from a project path using Claude Code’s slug convention (path separators → hyphens). E.g. ‘/Users/me/src/app` → `~/.claude/projects/-Users-me-src-app/memory`.



26
27
28
29
# File 'lib/claude_memory/hook/auto_memory_mirror.rb', line 26

def self.default_dir(project_path, claude_config_dir)
  slug = project_path.to_s.tr("/", "-")
  File.join(claude_config_dir, "projects", slug, "memory")
end

.default_state_file(project_path) ⇒ Object



31
32
33
# File 'lib/claude_memory/hook/auto_memory_mirror.rb', line 31

def self.default_state_file(project_path)
  File.join(project_path, ".claude", STATE_FILENAME)
end

Instance Method Details

#commit(candidates) ⇒ Object

Record the given candidates as the new baseline so they won’t be re-emitted until their content changes. Call only after the candidates have actually been surfaced to the user.



69
70
71
72
73
74
75
76
77
78
79
# File 'lib/claude_memory/hook/auto_memory_mirror.rb', line 69

def commit(candidates)
  return if candidates.empty?

  state = load_state
  candidates.each do |c|
    state[c[:name]] = {"md5" => c[:signature][:md5], "mtime" => c[:signature][:mtime]}
  end
  write_state(state)
rescue => e
  ClaudeMemory.logger.warn("AutoMemoryMirror#commit failed: #{e.message}")
end

#pending_candidates(limit: MAX_CANDIDATES) ⇒ Array<Hash>

Returns candidate entries — each path:, content:, signature:.

Returns:

  • (Array<Hash>)

    candidate entries — each path:, content:, signature:



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/claude_memory/hook/auto_memory_mirror.rb', line 41

def pending_candidates(limit: MAX_CANDIDATES)
  return [] unless Dir.exist?(@auto_memory_dir)

  state = load_state
  files = Dir.glob(File.join(@auto_memory_dir, "*.md")).sort_by { |p| -File.mtime(p).to_i }

  files.each_with_object([]) do |path, candidates|
    break candidates if candidates.size >= limit
    name = File.basename(path)
    signature = file_signature(path)
    prior = state[name]
    next if prior.is_a?(Hash) && prior["md5"] == signature[:md5]

    candidates << {
      name: name,
      path: path,
      content: Core::TextBuilder.truncate(safe_read(path), MAX_TEXT_PER_ITEM),
      signature: signature
    }
  end
rescue => e
  ClaudeMemory.logger.warn("AutoMemoryMirror#pending_candidates failed: #{e.message}")
  []
end