llmemory 0.1.13 → 0.1.15

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.
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "short_term/checkpoint"
4
+ require_relative "short_term/pruner"
4
5
  require_relative "long_term/file_based"
5
6
  require_relative "retrieval/engine"
6
7
 
@@ -22,7 +23,7 @@ module Llmemory
22
23
  def add_message(role:, content:)
23
24
  msgs = messages
24
25
  msgs << { role: role.to_sym, content: content.to_s }
25
- save_state(messages: msgs)
26
+ save_state(messages: msgs, **preserved_flush_state)
26
27
  true
27
28
  end
28
29
 
@@ -30,15 +31,47 @@ module Llmemory
30
31
  state = @checkpoint.restore_state
31
32
  return [] unless state.is_a?(Hash)
32
33
  list = state[STATE_KEY_MESSAGES] || state[STATE_KEY_MESSAGES.to_s]
33
- list.is_a?(Array) ? list.dup : []
34
+ list = list.is_a?(Array) ? list.dup : []
35
+ sanitize_messages(list)
34
36
  end
35
37
 
36
38
  def retrieve(query, max_tokens: nil)
37
- short_context = format_short_term_context(messages)
39
+ msgs = pruned_messages
40
+ short_context = format_short_term_context(msgs)
38
41
  long_context = @retrieval_engine.retrieve_for_inference(query, user_id: @user_id, max_tokens: max_tokens)
39
42
  combine_contexts(short_context, long_context)
40
43
  end
41
44
 
45
+ def recall_for(query: nil, max_tokens: nil)
46
+ return "" unless Llmemory.configuration.auto_recall_enabled
47
+
48
+ effective_query = query || last_user_message
49
+ return "" if effective_query.to_s.strip.empty?
50
+
51
+ retrieve(effective_query, max_tokens: max_tokens)
52
+ end
53
+
54
+ def last_user_message
55
+ msgs = messages
56
+ idx = msgs.rindex { |m| (m[:role] || m["role"]).to_s == "user" }
57
+ idx ? (msgs[idx][:content] || msgs[idx]["content"]).to_s : ""
58
+ end
59
+
60
+ def prune!(mode: nil)
61
+ return false unless Llmemory.configuration.prune_tool_results_enabled
62
+
63
+ msgs = messages
64
+ return false if msgs.empty?
65
+
66
+ mode ||= Llmemory.configuration.prune_tool_results_mode
67
+ pruner = ShortTerm::Pruner.new(
68
+ soft_trim_max_bytes: Llmemory.configuration.prune_tool_results_max_bytes
69
+ )
70
+ pruned = pruner.prune!(msgs, mode: mode)
71
+ save_state(messages: pruned, **preserved_flush_state)
72
+ true
73
+ end
74
+
42
75
  def consolidate!
43
76
  msgs = messages
44
77
  return true if msgs.empty?
@@ -58,15 +91,80 @@ module Llmemory
58
91
  current_bytes = messages_byte_size(msgs)
59
92
  return false if current_bytes <= max
60
93
 
94
+ flushed = flush_memory_before_compaction!(msgs)
95
+
61
96
  old_msgs, recent_msgs = split_messages_by_bytes(msgs, max)
62
97
  return false if old_msgs.empty?
63
98
 
64
99
  summary = summarize_messages(old_msgs)
65
100
  compacted = [{ role: :system, content: summary }] + recent_msgs
66
- save_state(messages: compacted)
101
+ state = restore_state_for_save
102
+ flush_ts = flushed ? Time.now : (state[:last_flush_at] || state["last_flush_at"])
103
+ save_state(messages: compacted, last_compact_at: Time.now, last_flush_at: flush_ts)
67
104
  true
68
105
  end
69
106
 
107
+ def maybe_flush_memory!
108
+ return false unless Llmemory.configuration.memory_flush_enabled
109
+ msgs = messages
110
+ return false if msgs.empty?
111
+ return false if estimated_tokens(msgs) < Llmemory.configuration.memory_flush_threshold_tokens
112
+
113
+ consolidate!
114
+ end
115
+
116
+ def context_tokens
117
+ estimated_tokens(messages)
118
+ end
119
+
120
+ def should_auto_consolidate?
121
+ ctx = context_tokens
122
+ threshold = Llmemory.configuration.context_window_tokens - Llmemory.configuration.reserve_tokens
123
+ ctx >= threshold
124
+ end
125
+
126
+ def should_compact?
127
+ ctx = context_tokens
128
+ threshold = Llmemory.configuration.context_window_tokens - Llmemory.configuration.reserve_tokens
129
+ ctx >= threshold
130
+ end
131
+
132
+ def with_overflow_recovery(max_retries: 2, &block)
133
+ return yield unless Llmemory.configuration.overflow_recovery_enabled
134
+ return yield unless block_given?
135
+
136
+ retries = 0
137
+ begin
138
+ yield
139
+ rescue Llmemory::LLMError => e
140
+ msg = e.message.to_s.downcase
141
+ overflow = msg.include?("context") || msg.include?("token") || msg.include?("overflow") || msg.include?("limit")
142
+ raise unless overflow && retries < max_retries
143
+
144
+ prune! if Llmemory.configuration.prune_tool_results_enabled
145
+ compact!
146
+ retries += 1
147
+ retry
148
+ end
149
+ end
150
+
151
+ def check_context_window!
152
+ return false if messages.empty?
153
+
154
+ flushed = false
155
+ if should_auto_consolidate? && Llmemory.configuration.memory_flush_enabled
156
+ consolidate!
157
+ flushed = true
158
+ end
159
+
160
+ compacted = false
161
+ if should_compact?
162
+ compacted = compact!
163
+ end
164
+
165
+ flushed || compacted
166
+ end
167
+
70
168
  def user_id
71
169
  @user_id
72
170
  end
@@ -92,6 +190,47 @@ module Llmemory
92
190
  @llm ||= Llmemory::LLM.client
93
191
  end
94
192
 
193
+ def flush_memory_before_compaction!(msgs)
194
+ return false unless Llmemory.configuration.memory_flush_enabled
195
+ return false if msgs.empty?
196
+ return false if estimated_tokens(msgs) < Llmemory.configuration.memory_flush_threshold_tokens
197
+
198
+ state = restore_state_for_save
199
+ last_compact = state[:last_compact_at] || state["last_compact_at"]
200
+ window = Llmemory.configuration.flush_once_per_cycle_seconds.to_i
201
+
202
+ if last_compact
203
+ t = last_compact.is_a?(Time) ? last_compact : Time.parse(last_compact.to_s)
204
+ return false if (Time.now - t).to_i < window
205
+ end
206
+
207
+ consolidate!
208
+ true
209
+ end
210
+
211
+ def sanitize_messages(msgs)
212
+ return msgs unless Llmemory.configuration.message_sanitizer_enabled
213
+
214
+ sanitizer = ShortTerm::MessageSanitizer.new
215
+ sanitizer.sanitize!(msgs)
216
+ end
217
+
218
+ def restore_state_for_save
219
+ @checkpoint.restore_state || {}
220
+ end
221
+
222
+ def preserved_flush_state
223
+ state = restore_state_for_save
224
+ {}.tap do |h|
225
+ h[:last_flush_at] = state[:last_flush_at] || state["last_flush_at"] if state[:last_flush_at] || state["last_flush_at"]
226
+ h[:last_compact_at] = state[:last_compact_at] || state["last_compact_at"] if state[:last_compact_at] || state["last_compact_at"]
227
+ end
228
+ end
229
+
230
+ def estimated_tokens(msgs)
231
+ (messages_byte_size(msgs) / 4.0).ceil
232
+ end
233
+
95
234
  def messages_byte_size(msgs)
96
235
  msgs.sum { |m| message_byte_size(m) }
97
236
  end
@@ -137,8 +276,20 @@ module Llmemory
137
276
  end
138
277
  end
139
278
 
140
- def save_state(messages:)
141
- @checkpoint.save_state(STATE_KEY_MESSAGES => messages)
279
+ def save_state(messages:, last_flush_at: nil, last_compact_at: nil)
280
+ state = { STATE_KEY_MESSAGES => messages, last_activity_at: Time.now }
281
+ state[:last_flush_at] = last_flush_at if last_flush_at
282
+ state[:last_compact_at] = last_compact_at if last_compact_at
283
+ @checkpoint.save_state(state)
284
+ end
285
+
286
+ def pruned_messages
287
+ return messages unless Llmemory.configuration.prune_tool_results_enabled
288
+
289
+ pruner = ShortTerm::Pruner.new(
290
+ soft_trim_max_bytes: Llmemory.configuration.prune_tool_results_max_bytes
291
+ )
292
+ pruner.prune!(messages, mode: Llmemory.configuration.prune_tool_results_mode)
142
293
  end
143
294
 
144
295
  def format_short_term_context(msgs)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ class NoiseFilter
5
+ NO_REPLY_MARKER = "NO_REPLY"
6
+ DEFAULT_MIN_CHARS = 10
7
+
8
+ def initialize(min_chars: nil, enabled: true)
9
+ @min_chars = min_chars || Llmemory.configuration.noise_filter_min_chars
10
+ @enabled = enabled
11
+ end
12
+
13
+ def filter(conversation_text)
14
+ return conversation_text.to_s unless @enabled
15
+
16
+ lines = conversation_text.to_s.split("\n")
17
+ seen = {}
18
+ filtered = lines.select do |line|
19
+ next false if line.strip.length < @min_chars
20
+ next false if line.include?(NO_REPLY_MARKER)
21
+ next false if seen[line.strip]
22
+
23
+ seen[line.strip] = true
24
+ true
25
+ end
26
+
27
+ filtered.join("\n").strip
28
+ end
29
+
30
+ def self.filter?(conversation_text)
31
+ return conversation_text.to_s unless Llmemory.configuration.noise_filter_enabled
32
+
33
+ new.filter(conversation_text)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Retrieval
5
+ class Bm25Scorer
6
+ K1 = 1.5
7
+ B = 0.75
8
+
9
+ def initialize(k1: K1, b: B)
10
+ @k1 = k1
11
+ @b = b
12
+ end
13
+
14
+ def score_candidates(query, candidates)
15
+ return [] if candidates.empty?
16
+
17
+ query_tokens = tokenize(query)
18
+ return candidates.map { |c| c.merge(bm25_score: 0.0, normalized_bm25: 0.0) } if query_tokens.empty?
19
+
20
+ doc_tokens_list = candidates.map { |c| tokenize((c[:text] || c["text"]).to_s) }
21
+ avg_doc_len = doc_tokens_list.map(&:size).sum.to_f / [doc_tokens_list.size, 1].max
22
+ n_docs = candidates.size
23
+
24
+ doc_freq = Hash.new(0)
25
+ doc_tokens_list.each do |tokens|
26
+ tokens.uniq.each { |t| doc_freq[t] += 1 }
27
+ end
28
+
29
+ candidates.each_with_index.map do |c, i|
30
+ doc_tokens = doc_tokens_list[i]
31
+ doc_len = doc_tokens.size
32
+ bm25 = 0.0
33
+
34
+ query_tokens.uniq.each do |term|
35
+ tf = doc_tokens.count(term)
36
+ next if tf.zero?
37
+
38
+ n_qi = doc_freq[term]
39
+ idf = Math.log((n_docs - n_qi + 0.5) / (n_qi + 0.5) + 1.0)
40
+ numerator = tf * (@k1 + 1)
41
+ denom = tf + @k1 * (1 - @b + @b * doc_len.to_f / [avg_doc_len, 1].max)
42
+ bm25 += idf * numerator / denom
43
+ end
44
+
45
+ c.merge(bm25_score: bm25)
46
+ end.tap do |scored|
47
+ max_bm25 = scored.map { |s| s[:bm25_score] }.max.to_f
48
+ max_bm25 = 1.0 if max_bm25.zero?
49
+ scored.each { |s| s[:normalized_bm25] = s[:bm25_score] / max_bm25 }
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def tokenize(text)
56
+ text.to_s.downcase.scan(/\b[a-z0-9]{2,}\b/)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative "temporal_ranker"
4
4
  require_relative "context_assembler"
5
+ require_relative "bm25_scorer"
6
+ require_relative "mmr_reranker"
5
7
 
6
8
  module Llmemory
7
9
  module Retrieval
@@ -13,15 +15,19 @@ module Llmemory
13
15
  @llm = llm || Llmemory::LLM.client
14
16
  @ranker = TemporalRanker.new
15
17
  @assembler = ContextAssembler.new
18
+ @bm25_scorer = Bm25Scorer.new
19
+ @mmr_reranker = MmrReranker.new(lambda: Llmemory.configuration.mmr_lambda)
16
20
  end
17
21
 
18
22
  def retrieve_for_inference(user_message, user_id: nil, max_tokens: nil)
19
23
  user_id ||= @memory.respond_to?(:user_id) ? @memory.user_id : nil
20
24
  search_query = generate_query(user_message)
21
25
  candidates = fetch_candidates(search_query, user_id)
26
+ candidates = apply_hybrid_scoring(candidates, search_query) if Llmemory.configuration.hybrid_search_enabled
22
27
 
23
28
  relevant = filter_by_relevance(candidates, user_message)
24
29
  ranked = @ranker.rank(relevant)
30
+ ranked = @mmr_reranker.rerank(ranked) if Llmemory.configuration.mmr_enabled
25
31
  @assembler.assemble(ranked, max_tokens: max_tokens)
26
32
  end
27
33
 
@@ -47,7 +53,8 @@ module Llmemory
47
53
  {
48
54
  text: c[:text] || c["text"],
49
55
  timestamp: parse_timestamp(c[:timestamp] || c["timestamp"] || c[:created_at] || c["created_at"]),
50
- score: (c[:score] || c["score"] || 1.0).to_f
56
+ score: (c[:score] || c["score"] || 1.0).to_f,
57
+ evergreen: c[:evergreen] || c["evergreen"]
51
58
  }
52
59
  end
53
60
  end
@@ -58,6 +65,21 @@ module Llmemory
58
65
  Time.now
59
66
  end
60
67
 
68
+ def apply_hybrid_scoring(candidates, query)
69
+ return candidates if candidates.empty?
70
+
71
+ scored = @bm25_scorer.score_candidates(query, candidates)
72
+ weight = Llmemory.configuration.bm25_weight.to_f
73
+ weight = 0.3 if weight < 0 || weight > 1
74
+
75
+ scored.map do |c|
76
+ vector_score = (c[:score] || c["score"] || 1.0).to_f
77
+ bm25_norm = (c[:normalized_bm25] || 0).to_f
78
+ hybrid = weight * bm25_norm + (1 - weight) * vector_score
79
+ c.merge(score: hybrid)
80
+ end
81
+ end
82
+
61
83
  def filter_by_relevance(candidates, user_message)
62
84
  return candidates if candidates.size <= 5
63
85
  user_lower = user_message.to_s.downcase
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Retrieval
5
+ class MmrReranker
6
+ def initialize(lambda: 0.7)
7
+ @lambda = lambda
8
+ end
9
+
10
+ def rerank(candidates, score_key: :temporal_score)
11
+ return candidates if candidates.size <= 1
12
+
13
+ selected = []
14
+ remaining = candidates.dup
15
+
16
+ while remaining.any?
17
+ best_idx = nil
18
+ best_mmr = -Float::INFINITY
19
+
20
+ remaining.each_with_index do |cand, i|
21
+ rel = (cand[score_key] || cand[score_key.to_s] || cand[:score] || cand["score"] || 0).to_f
22
+ max_sim = selected.map { |s| similarity(cand, s) }.max || 0
23
+ mmr = @lambda * rel - (1 - @lambda) * max_sim
24
+
25
+ if mmr > best_mmr
26
+ best_mmr = mmr
27
+ best_idx = i
28
+ end
29
+ end
30
+
31
+ break unless best_idx
32
+
33
+ selected << remaining.delete_at(best_idx)
34
+ end
35
+
36
+ selected
37
+ end
38
+
39
+ private
40
+
41
+ def similarity(a, b)
42
+ text_a = tokenize((a[:text] || a["text"]).to_s)
43
+ text_b = tokenize((b[:text] || b["text"]).to_s)
44
+ return 0.0 if text_a.empty? || text_b.empty?
45
+
46
+ intersection = (text_a & text_b).size
47
+ union = (text_a | text_b).size
48
+ union.zero? ? 0.0 : intersection.to_f / union
49
+ end
50
+
51
+ def tokenize(text)
52
+ text.downcase.scan(/\b[a-z0-9]{2,}\b/).uniq
53
+ end
54
+ end
55
+ end
56
+ end
@@ -8,12 +8,20 @@ module Llmemory
8
8
  end
9
9
 
10
10
  def rank(candidates, now: Time.now)
11
+ lambda_val = Math.log(2) / @half_life_days.to_f
12
+
11
13
  candidates.map do |c|
12
14
  score = (c[:score] || c["score"] || 1.0).to_f
13
15
  timestamp = c[:timestamp] || c["timestamp"]
14
16
  timestamp = Time.parse(timestamp.to_s) if timestamp.is_a?(String)
15
17
  age_days = timestamp ? (now - timestamp).to_i / 86400 : 0
16
- time_decay = 1.0 / (1.0 + (age_days.to_f / @half_life_days))
18
+
19
+ time_decay = if c[:evergreen] || c["evergreen"]
20
+ 1.0
21
+ else
22
+ Math.exp(-lambda_val * age_days.to_f)
23
+ end
24
+
17
25
  final_score = score * time_decay
18
26
  c.merge(score: score, temporal_score: final_score, timestamp: timestamp)
19
27
  end.sort_by { |c| -(c[:temporal_score] || 0) }
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module ShortTerm
5
+ class MessageSanitizer
6
+ def initialize(max_message_chars: nil)
7
+ @max_chars = max_message_chars || Llmemory.configuration.max_message_chars
8
+ end
9
+
10
+ def sanitize!(messages)
11
+ return [] if messages.nil? || !messages.is_a?(Array)
12
+
13
+ out = []
14
+ expect_tool_result = false
15
+
16
+ messages.each do |msg|
17
+ msg = msg.dup
18
+ content = (msg[:content] || msg["content"]).to_s
19
+ role = (msg[:role] || msg["role"]).to_s
20
+
21
+ next if content.strip.empty?
22
+
23
+ content = content[0, @max_chars] if @max_chars && content.length > @max_chars
24
+
25
+ if role == "tool"
26
+ expect_tool_result = true
27
+ elsif role == "tool_result"
28
+ next unless expect_tool_result
29
+ expect_tool_result = false
30
+ else
31
+ expect_tool_result = false
32
+ end
33
+
34
+ msg[:content] = content if msg.key?(:content)
35
+ msg["content"] = content if msg.key?("content")
36
+ out << msg
37
+ end
38
+
39
+ out
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module ShortTerm
5
+ class Pruner
6
+ DEFAULT_PRUNABLE_ROLES = %i[tool tool_result].freeze
7
+ PLACEHOLDER = "[Tool result pruned]"
8
+
9
+ def initialize(prunable_roles: nil, soft_trim_max_bytes: 2048, soft_trim_head_ratio: 0.4, soft_trim_tail_ratio: 0.2)
10
+ @prunable_roles = prunable_roles || DEFAULT_PRUNABLE_ROLES.map(&:to_s)
11
+ @soft_trim_max_bytes = soft_trim_max_bytes
12
+ @head_ratio = soft_trim_head_ratio
13
+ @tail_ratio = soft_trim_tail_ratio
14
+ end
15
+
16
+ def prune!(messages, mode: :soft_trim)
17
+ return messages.dup if messages.empty?
18
+
19
+ messages.map do |msg|
20
+ if prunable?(msg)
21
+ apply_prune(msg, mode)
22
+ else
23
+ msg.dup
24
+ end
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def prunable?(msg)
31
+ role = (msg[:role] || msg["role"]).to_s
32
+ @prunable_roles.include?(role)
33
+ end
34
+
35
+ def apply_prune(msg, mode)
36
+ content = (msg[:content] || msg["content"]).to_s
37
+ new_content = case mode.to_s.to_sym
38
+ when :hard_clear
39
+ PLACEHOLDER
40
+ when :soft_trim
41
+ soft_trim(content)
42
+ else
43
+ content
44
+ end
45
+
46
+ result = msg.dup
47
+ result[:content] = new_content if result.key?(:content)
48
+ result["content"] = new_content if result.key?("content")
49
+ result
50
+ end
51
+
52
+ def soft_trim(content)
53
+ return content if content.bytesize <= @soft_trim_max_bytes
54
+
55
+ head_chars = (@soft_trim_max_bytes * @head_ratio).to_i
56
+ tail_chars = (@soft_trim_max_bytes * @tail_ratio).to_i
57
+
58
+ head = content.byteslice(0, head_chars)
59
+ tail = content.bytesize > (head_chars + tail_chars) ? content.byteslice(-tail_chars, tail_chars) : ""
60
+
61
+ "#{head}\n...\n#{tail}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module ShortTerm
5
+ class SessionLifecycle
6
+ def initialize(store: nil)
7
+ @store = store || build_store
8
+ end
9
+
10
+ def cleanup_idle_sessions!(user_id:, idle_minutes: nil)
11
+ idle_minutes ||= Llmemory.configuration.session_idle_minutes
12
+ cutoff = Time.now - (idle_minutes * 60)
13
+ deleted = 0
14
+
15
+ @store.list_sessions(user_id: user_id).each do |session_id|
16
+ state = @store.load(user_id, session_id)
17
+ next unless state.is_a?(Hash)
18
+
19
+ last_activity = state[:last_activity_at] || state["last_activity_at"]
20
+ next if last_activity.nil?
21
+
22
+ last_time = last_activity.is_a?(Time) ? last_activity : Time.parse(last_activity.to_s)
23
+ if last_time < cutoff
24
+ @store.delete(user_id, session_id)
25
+ deleted += 1
26
+ end
27
+ end
28
+
29
+ deleted
30
+ end
31
+
32
+ def cleanup_stale_sessions!(user_id:, prune_after_days: nil)
33
+ prune_after_days ||= Llmemory.configuration.session_prune_after_days
34
+ cutoff = Time.now - (prune_after_days * 86400)
35
+ deleted = 0
36
+
37
+ @store.list_sessions(user_id: user_id).each do |session_id|
38
+ state = @store.load(user_id, session_id)
39
+ next unless state.is_a?(Hash)
40
+
41
+ last_activity = state[:last_activity_at] || state["last_activity_at"]
42
+ next if last_activity.nil?
43
+
44
+ last_time = last_activity.is_a?(Time) ? last_activity : Time.parse(last_activity.to_s)
45
+ if last_time < cutoff
46
+ @store.delete(user_id, session_id)
47
+ deleted += 1
48
+ end
49
+ end
50
+
51
+ deleted
52
+ end
53
+
54
+ def enforce_max_entries!(user_id:, max_entries: nil)
55
+ max_entries ||= Llmemory.configuration.session_max_entries_per_user
56
+ sessions = @store.list_sessions(user_id: user_id)
57
+ return 0 if sessions.size <= max_entries
58
+
59
+ session_ages = sessions.map do |session_id|
60
+ state = @store.load(user_id, session_id)
61
+ last_activity = state&.dig(:last_activity_at) || state&.dig("last_activity_at")
62
+ last_time = last_activity.is_a?(Time) ? last_activity : (last_activity ? Time.parse(last_activity.to_s) : Time.at(0))
63
+ [session_id, last_time]
64
+ end
65
+
66
+ session_ages.sort_by! { |_, t| t }
67
+ to_delete = session_ages.first(session_ages.size - max_entries).map(&:first)
68
+ to_delete.each { |sid| @store.delete(user_id, sid) }
69
+ to_delete.size
70
+ end
71
+
72
+ private
73
+
74
+ def build_store
75
+ case Llmemory.configuration.short_term_store.to_sym
76
+ when :memory then Stores::MemoryStore.new
77
+ when :redis then Stores::RedisStore.new
78
+ when :postgres then Stores::PostgresStore.new
79
+ when :active_record, :activerecord
80
+ require_relative "stores/active_record_store"
81
+ Stores::ActiveRecordStore.new
82
+ else
83
+ Stores::MemoryStore.new
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "short_term/checkpoint"
4
+ require_relative "short_term/session_lifecycle"
5
+ require_relative "short_term/message_sanitizer"
4
6
 
5
7
  module Llmemory
6
8
  module ShortTerm