Class: ClaudeMemory::Hook::ContextInjector

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

Overview

Generates context for SessionStart hook injection. Queries both global and project databases for key facts and formats them as concise context for Claude.

Constant Summary collapse

MAX_DECISIONS =
5
MAX_CONVENTIONS =
5
MAX_ARCHITECTURE =
5
MAX_OBSERVATIONS =
10
MAX_PROMOTION_CANDIDATES =
5
MAX_UNDISTILLED =
3
MAX_TEXT_PER_ITEM =
1500
MAX_MIRROR_CANDIDATES =
5
FRESH_SESSION_SOURCES =
%w[startup resume clear].freeze
QUERIES =
{
  decisions: {query: "decision constraint rule requirement", scope: "all"},
  conventions: {query: "convention style format pattern prefer", scope: "all"},
  architecture: {query: "uses framework implements architecture pattern", scope: "all"}
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(manager, source: nil, auto_memory_mirror: nil, stale_threshold_days: nil) ⇒ ContextInjector

Returns a new instance of ContextInjector.



38
39
40
41
42
43
44
45
46
47
48
# File 'lib/claude_memory/hook/context_injector.rb', line 38

def initialize(manager, source: nil, auto_memory_mirror: nil, stale_threshold_days: nil)
  @manager = manager
  @source = source
  @recall = Recall.new(manager)
  @auto_memory_mirror = auto_memory_mirror
  @stale_threshold_days = stale_threshold_days
  @emitted_fact_ids = []
  @emitted_subjects = []
  @emitted_facts_by_scope = Hash.new { |h, k| h[k] = [] }
  @emitted_observation_count = 0
end

Instance Attribute Details

#emitted_fact_idsObject (readonly)

Fact IDs and subjects that ‘generate_context` injected on the most recent call. Both are empty until `generate_context` has been invoked. Populated in call order (decisions → conventions → architecture) so benchmark harnesses can attribute sections if they care.

emitted_facts_by_scope groups the IDs by the DB they came from (=> […], “global” => […]) so telemetry can resolve each fact from the correct store. Fact IDs autoincrement per-DB, so a bare ID without scope is ambiguous.



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

def emitted_fact_ids
  @emitted_fact_ids
end

#emitted_facts_by_scopeObject (readonly)

Fact IDs and subjects that ‘generate_context` injected on the most recent call. Both are empty until `generate_context` has been invoked. Populated in call order (decisions → conventions → architecture) so benchmark harnesses can attribute sections if they care.

emitted_facts_by_scope groups the IDs by the DB they came from (=> […], “global” => […]) so telemetry can resolve each fact from the correct store. Fact IDs autoincrement per-DB, so a bare ID without scope is ambiguous.



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

def emitted_facts_by_scope
  @emitted_facts_by_scope
end

#emitted_observation_countObject (readonly)

Fact IDs and subjects that ‘generate_context` injected on the most recent call. Both are empty until `generate_context` has been invoked. Populated in call order (decisions → conventions → architecture) so benchmark harnesses can attribute sections if they care.

emitted_facts_by_scope groups the IDs by the DB they came from (=> […], “global” => […]) so telemetry can resolve each fact from the correct store. Fact IDs autoincrement per-DB, so a bare ID without scope is ambiguous.



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

def emitted_observation_count
  @emitted_observation_count
end

#emitted_subjectsObject (readonly)

Fact IDs and subjects that ‘generate_context` injected on the most recent call. Both are empty until `generate_context` has been invoked. Populated in call order (decisions → conventions → architecture) so benchmark harnesses can attribute sections if they care.

emitted_facts_by_scope groups the IDs by the DB they came from (=> […], “global” => […]) so telemetry can resolve each fact from the correct store. Fact IDs autoincrement per-DB, so a bare ID without scope is ambiguous.



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

def emitted_subjects
  @emitted_subjects
end

Instance Method Details

#generate_contextObject



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/claude_memory/hook/context_injector.rb', line 50

def generate_context
  @emitted_fact_ids = []
  @emitted_subjects = []
  @emitted_facts_by_scope = Hash.new { |h, k| h[k] = [] }
  @emitted_observation_count = 0
  sections = []

  decisions = fetch(:decisions, MAX_DECISIONS)
  sections << format_section("Decisions", decisions) if decisions.any?

  conventions = fetch(:conventions, MAX_CONVENTIONS)
  sections << format_section("Conventions", conventions) if conventions.any?

  architecture = fetch(:architecture, MAX_ARCHITECTURE)
  sections << format_section("Architecture", architecture) if architecture.any?

  # Block 1 of the two-block context: the episodic observation log. Sits
  # ahead of the (fresh-session) undistilled "Pending Knowledge Extraction"
  # tail (Block 2). Newest-first; only 🔴 carries a marker for the actor.
  observations = fetch_observations(MAX_OBSERVATIONS)
  @emitted_observation_count = observations.size
  obs_section = Observe::ObservationsRenderer.render(observations)
  sections << obs_section if obs_section

  if fresh_session?
    undistilled = fetch_undistilled(MAX_UNDISTILLED)
    sections << format_distillation_prompt(undistilled) if undistilled.any?

    promotion = fetch_promotion_candidates(MAX_PROMOTION_CANDIDATES)
    sections << format_observation_reflection(promotion) if promotion.any?

    mirror_candidates = fetch_mirror_candidates(MAX_MIRROR_CANDIDATES)
    if mirror_candidates.any?
      sections << format_auto_memory_mirror(mirror_candidates)
      auto_memory_mirror.commit(mirror_candidates)
    end
  end

  return nil if sections.empty?

  sections.join("\n")
end

#reflection_contextString?

Reflection-only context for PreCompact (context pressure) — just the promote/consolidate instructions for corroborated/related observations. PreCompact is the analog of Mastra’s token-threshold reflection trigger; at compaction we nudge the Claude-as-reflector pass rather than re-inject the full snapshot (which would add tokens as the window fills).

Returns:

  • (String, nil)


99
100
101
102
103
104
# File 'lib/claude_memory/hook/context_injector.rb', line 99

def reflection_context
  candidates = fetch_promotion_candidates(MAX_PROMOTION_CANDIDATES)
  return nil if candidates.empty?

  format_observation_reflection(candidates)
end