llmemory 0.2.10 → 0.3.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -0
  3. data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +71 -0
  4. data/lib/generators/llmemory/upgrade_zero_mem/templates/create_llmemory_zero_mem_tables.rb +77 -0
  5. data/lib/generators/llmemory/upgrade_zero_mem/upgrade_zero_mem_generator.rb +19 -0
  6. data/lib/llmemory/cli/commands/zero_mem.rb +107 -0
  7. data/lib/llmemory/cli.rb +3 -0
  8. data/lib/llmemory/configuration.rb +67 -1
  9. data/lib/llmemory/consolidation/filter.rb +74 -0
  10. data/lib/llmemory/consolidation/policy.rb +17 -0
  11. data/lib/llmemory/consolidation/rule_based_policy.rb +63 -0
  12. data/lib/llmemory/consolidation/unit.rb +54 -0
  13. data/lib/llmemory/consolidation.rb +11 -0
  14. data/lib/llmemory/instrumentation.rb +10 -1
  15. data/lib/llmemory/long_term/file_based/memory.rb +18 -7
  16. data/lib/llmemory/long_term/graph_based/memory.rb +31 -8
  17. data/lib/llmemory/maintenance/cognitive_pass.rb +29 -1
  18. data/lib/llmemory/maintenance/policy_prune.rb +85 -0
  19. data/lib/llmemory/maintenance/zero_mem_ttl.rb +26 -0
  20. data/lib/llmemory/maintenance.rb +2 -0
  21. data/lib/llmemory/mcp/server.rb +2 -0
  22. data/lib/llmemory/mcp/store_helpers.rb +16 -0
  23. data/lib/llmemory/mcp/tools/memory_add_message.rb +17 -4
  24. data/lib/llmemory/mcp/tools/memory_consolidate.rb +9 -4
  25. data/lib/llmemory/mcp/tools/memory_maintain.rb +6 -1
  26. data/lib/llmemory/mcp/tools/memory_retrieve.rb +3 -1
  27. data/lib/llmemory/mcp/tools/memory_retrieve_evidence.rb +70 -0
  28. data/lib/llmemory/mcp/tools/memory_stats.rb +22 -0
  29. data/lib/llmemory/memory.rb +257 -9
  30. data/lib/llmemory/retrieval/bm25_scorer.rb +5 -0
  31. data/lib/llmemory/retrieval/context_assembler.rb +17 -1
  32. data/lib/llmemory/tokenizer.rb +18 -8
  33. data/lib/llmemory/vector_store/memory_store.rb +23 -1
  34. data/lib/llmemory/vector_store/openai_embeddings.rb +47 -21
  35. data/lib/llmemory/version.rb +1 -1
  36. data/lib/llmemory/zero_mem/answer_calibrator.rb +63 -0
  37. data/lib/llmemory/zero_mem/backfill.rb +43 -0
  38. data/lib/llmemory/zero_mem/budget_assembler.rb +62 -0
  39. data/lib/llmemory/zero_mem/circuit_breaker.rb +37 -0
  40. data/lib/llmemory/zero_mem/embedding_provider.rb +73 -0
  41. data/lib/llmemory/zero_mem/engine.rb +247 -0
  42. data/lib/llmemory/zero_mem/entity.rb +31 -0
  43. data/lib/llmemory/zero_mem/entity_extractor.rb +23 -0
  44. data/lib/llmemory/zero_mem/entity_index.rb +65 -0
  45. data/lib/llmemory/zero_mem/entity_mention.rb +35 -0
  46. data/lib/llmemory/zero_mem/errors.rb +7 -0
  47. data/lib/llmemory/zero_mem/evidence.rb +43 -0
  48. data/lib/llmemory/zero_mem/evidence_calibrator.rb +158 -0
  49. data/lib/llmemory/zero_mem/evidence_closure.rb +98 -0
  50. data/lib/llmemory/zero_mem/evidence_fusion.rb +57 -0
  51. data/lib/llmemory/zero_mem/evidence_set.rb +54 -0
  52. data/lib/llmemory/zero_mem/extractors/heuristic.rb +44 -0
  53. data/lib/llmemory/zero_mem/extractors/http.rb +58 -0
  54. data/lib/llmemory/zero_mem/graph_retriever.rb +107 -0
  55. data/lib/llmemory/zero_mem/hierarchy_builder.rb +123 -0
  56. data/lib/llmemory/zero_mem/hierarchy_retriever.rb +133 -0
  57. data/lib/llmemory/zero_mem/indexer.rb +73 -0
  58. data/lib/llmemory/zero_mem/mode.rb +30 -0
  59. data/lib/llmemory/zero_mem/pagerank.rb +42 -0
  60. data/lib/llmemory/zero_mem/query_profile.rb +48 -0
  61. data/lib/llmemory/zero_mem/query_profiler.rb +138 -0
  62. data/lib/llmemory/zero_mem/repair.rb +31 -0
  63. data/lib/llmemory/zero_mem/router.rb +69 -0
  64. data/lib/llmemory/zero_mem/sidecar_client.rb +82 -0
  65. data/lib/llmemory/zero_mem/storage.rb +124 -0
  66. data/lib/llmemory/zero_mem/storages/active_record.rb +311 -0
  67. data/lib/llmemory/zero_mem/storages/active_record_models.rb +21 -0
  68. data/lib/llmemory/zero_mem/storages/file.rb +67 -0
  69. data/lib/llmemory/zero_mem/storages/memory.rb +294 -0
  70. data/lib/llmemory/zero_mem/storages/postgres.rb +85 -0
  71. data/lib/llmemory/zero_mem/storages/snapshot_codec.rb +180 -0
  72. data/lib/llmemory/zero_mem/storages.rb +43 -0
  73. data/lib/llmemory/zero_mem/trace.rb +100 -0
  74. data/lib/llmemory/zero_mem/trace_state_link.rb +64 -0
  75. data/lib/llmemory/zero_mem/trace_unit.rb +83 -0
  76. data/lib/llmemory/zero_mem.rb +42 -0
  77. data/lib/llmemory.rb +8 -0
  78. data/lib/tasks/llmemory.rake +84 -2
  79. metadata +54 -2
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module ZeroMem
5
+ # Append-only trace storage contract (ZM1).
6
+ module Storage
7
+ def write_trace(trace)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def get_trace(user_id, trace_id)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def find_by_idempotency_key(user_id, idempotency_key)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def next_sequence(user_id, session_id)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def list_traces(user_id, session_id: nil, include_archived: false, limit: nil, offset: nil)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def archive_trace(user_id, trace_id, archived_at: Time.now)
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def write_state_link(link)
32
+ raise NotImplementedError
33
+ end
34
+
35
+ def close_open_state_links(user_id, state_key, valid_to:, except_trace_id: nil)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def state_links_for(user_id, state_key)
40
+ raise NotImplementedError
41
+ end
42
+
43
+ def current_state_trace_id(user_id, state_key, as_of: Time.now)
44
+ raise NotImplementedError
45
+ end
46
+
47
+ def get_watermark(user_id, session_id)
48
+ raise NotImplementedError
49
+ end
50
+
51
+ def set_watermark(user_id, session_id, last_sequence:, index_version:)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def search_traces_by_tokens(user_id, query, limit: 20)
56
+ raise NotImplementedError
57
+ end
58
+
59
+ def write_unit(unit)
60
+ raise NotImplementedError
61
+ end
62
+
63
+ def list_units(user_id, kind: nil, session_id: nil, boundary_id: nil)
64
+ raise NotImplementedError
65
+ end
66
+
67
+ def get_unit(user_id, unit_id)
68
+ raise NotImplementedError
69
+ end
70
+
71
+ def open_episode(user_id, episode_key)
72
+ raise NotImplementedError
73
+ end
74
+
75
+ def set_open_episode(user_id, episode_key, unit_id)
76
+ raise NotImplementedError
77
+ end
78
+
79
+ def close_episode(unit_id)
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def upsert_entity(entity)
84
+ raise NotImplementedError
85
+ end
86
+
87
+ def write_mention(mention)
88
+ raise NotImplementedError
89
+ end
90
+
91
+ def entity_exists?(user_id, normalized_key)
92
+ raise NotImplementedError
93
+ end
94
+
95
+ def trace_ids_for_entity_key(user_id, normalized_key)
96
+ raise NotImplementedError
97
+ end
98
+
99
+ def entity_keys_for_trace(user_id, trace_id)
100
+ raise NotImplementedError
101
+ end
102
+
103
+ def previous_trace(user_id, session_id, sequence)
104
+ raise NotImplementedError
105
+ end
106
+
107
+ def link_adjacent_traces(user_id, prev_trace_id, trace_id)
108
+ raise NotImplementedError
109
+ end
110
+
111
+ def adjacent_traces(user_id, trace_id)
112
+ raise NotImplementedError
113
+ end
114
+
115
+ def store_turn_embedding(user_id, trace_id, vector, model:, dimensions:)
116
+ raise NotImplementedError
117
+ end
118
+
119
+ def all_entity_keys(user_id)
120
+ raise NotImplementedError
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../storage"
5
+ require_relative "../../crypto/field_helpers"
6
+ require_relative "../../active_record_helpers"
7
+
8
+ module Llmemory
9
+ module ZeroMem
10
+ module Storages
11
+ class ActiveRecordStorage
12
+ include Storage
13
+ include Llmemory::Crypto::FieldHelpers
14
+ include Llmemory::ActiveRecordHelpers
15
+
16
+ def initialize(cipher: nil)
17
+ @cipher = cipher || Llmemory.build_cipher
18
+ self.class.load_models!
19
+ end
20
+
21
+ def self.load_models!
22
+ return if @models_loaded
23
+
24
+ require "active_record"
25
+ require_relative "active_record_models"
26
+ @models_loaded = true
27
+ end
28
+
29
+ def write_trace(trace)
30
+ t = trace.is_a?(Trace) ? trace : Trace.new(**trace)
31
+ if t.idempotency_key && (existing = find_by_idempotency_key(t.user_id, t.idempotency_key))
32
+ return existing.id
33
+ end
34
+
35
+ rec = LlmemoryTrace.find_or_initialize_by(id: t.id)
36
+ rec.user_id = t.user_id
37
+ rec.session_id = t.session_id
38
+ rec.boundary_id = t.boundary_id
39
+ rec.sequence = t.sequence
40
+ rec.role = t.role.to_s
41
+ rec.content = enc(t.content)
42
+ rec.content_sha256 = t.content_sha256
43
+ rec.occurred_at = t.occurred_at
44
+ rec.ingested_at = t.ingested_at
45
+ rec.metadata = metadata_column(t.metadata)
46
+ rec.idempotency_key = t.idempotency_key
47
+ rec.archived_at = t.archived_at
48
+ rec.search_tokens = search_tokens_for(t.content) if LlmemoryTrace.column_names.include?("search_tokens")
49
+ with_unique_retry { rec.save! }
50
+ t.id
51
+ end
52
+
53
+ def get_trace(user_id, trace_id)
54
+ rec = LlmemoryTrace.find_by(id: trace_id.to_s, user_id: user_id.to_s)
55
+ return nil unless rec
56
+
57
+ record_to_trace(rec)
58
+ end
59
+
60
+ def find_by_idempotency_key(user_id, idempotency_key)
61
+ return nil if idempotency_key.to_s.empty?
62
+
63
+ rec = LlmemoryTrace.find_by(user_id: user_id.to_s, idempotency_key: idempotency_key.to_s)
64
+ rec ? record_to_trace(rec) : nil
65
+ end
66
+
67
+ def next_sequence(user_id, session_id)
68
+ max = LlmemoryTrace.where(user_id: user_id.to_s, session_id: session_id.to_s).maximum(:sequence)
69
+ (max || 0) + 1
70
+ end
71
+
72
+ def list_traces(user_id, session_id: nil, include_archived: false, limit: nil, offset: nil)
73
+ scope = LlmemoryTrace.where(user_id: user_id.to_s)
74
+ scope = scope.where(session_id: session_id.to_s) if session_id
75
+ scope = scope.where(archived_at: nil) unless include_archived
76
+ scope = scope.order(:session_id, :sequence, :ingested_at)
77
+ scope = scope.offset(offset.to_i) if offset
78
+ scope = scope.limit(limit.to_i) if limit && limit.to_i.positive?
79
+ scope.map { |r| record_to_trace(r) }
80
+ end
81
+
82
+ def archive_trace(user_id, trace_id, archived_at: Time.now)
83
+ rec = LlmemoryTrace.find_by(id: trace_id.to_s, user_id: user_id.to_s, archived_at: nil)
84
+ return false unless rec
85
+
86
+ rec.update!(archived_at: archived_at)
87
+ true
88
+ end
89
+
90
+ def write_state_link(link)
91
+ rec = LlmemoryTraceStateLink.new(
92
+ user_id: link.user_id,
93
+ state_key: link.state_key,
94
+ trace_id: link.trace_id,
95
+ valid_from: link.valid_from,
96
+ valid_to: link.valid_to,
97
+ supersedes_trace_id: link.supersedes_trace_id,
98
+ source: link.source.to_s,
99
+ created_at: link.created_at
100
+ )
101
+ with_unique_retry { rec.save! }
102
+ link.trace_id
103
+ end
104
+
105
+ def close_open_state_links(user_id, state_key, valid_to:, except_trace_id: nil)
106
+ scope = LlmemoryTraceStateLink.where(user_id: user_id.to_s, state_key: state_key.to_s, valid_to: nil)
107
+ scope = scope.where.not(trace_id: except_trace_id.to_s) if except_trace_id
108
+ scope.update_all(valid_to: valid_to)
109
+ end
110
+
111
+ def state_links_for(user_id, state_key)
112
+ LlmemoryTraceStateLink.where(user_id: user_id.to_s, state_key: state_key.to_s)
113
+ .order(:valid_from, :created_at)
114
+ .map { |r| record_to_state_link(r) }
115
+ end
116
+
117
+ def current_state_trace_id(user_id, state_key, as_of: Time.now)
118
+ t = as_of.is_a?(Time) ? as_of : Time.parse(as_of.to_s)
119
+ link = LlmemoryTraceStateLink.where(user_id: user_id.to_s, state_key: state_key.to_s)
120
+ .where("valid_from <= ?", t)
121
+ .where("valid_to IS NULL OR valid_to >= ?", t)
122
+ .order(valid_from: :desc, created_at: :desc)
123
+ .first
124
+ link&.trace_id
125
+ end
126
+
127
+ def get_watermark(user_id, session_id)
128
+ rec = LlmemoryTrace.where(user_id: user_id.to_s, session_id: session_id.to_s)
129
+ .order(sequence: :desc)
130
+ .limit(1)
131
+ .pick(:sequence)
132
+ {
133
+ last_sequence: rec.to_i,
134
+ index_version: rec.to_i
135
+ }
136
+ end
137
+
138
+ def set_watermark(user_id, session_id, last_sequence:, index_version:)
139
+ # ZM1: watermark derived from traces; noop placeholder for repair/backfill API.
140
+ { last_sequence: last_sequence.to_i, index_version: index_version.to_i }
141
+ end
142
+
143
+ def write_unit(unit)
144
+ return unit.id unless defined?(LlmemoryTraceUnit)
145
+
146
+ data = unit.to_h
147
+ rec = LlmemoryTraceUnit.find_or_initialize_by(id: unit.id)
148
+ rec.user_id = unit.user_id
149
+ rec.trace_id = unit.member_trace_ids.first
150
+ rec.unit_type = unit.kind.to_s
151
+ rec.data = cipher.enabled? ? enc_json(data) : data
152
+ with_unique_retry { rec.save! }
153
+ unit.id
154
+ end
155
+
156
+ def list_units(user_id, kind: nil, session_id: nil, boundary_id: nil)
157
+ return [] unless defined?(LlmemoryTraceUnit)
158
+
159
+ scope = LlmemoryTraceUnit.where(user_id: user_id.to_s, archived_at: nil)
160
+ scope = scope.where(unit_type: kind.to_s) if kind
161
+ scope.map { |r| unit_from_record(r) }
162
+ end
163
+
164
+ def get_unit(user_id, unit_id)
165
+ return nil unless defined?(LlmemoryTraceUnit)
166
+
167
+ rec = LlmemoryTraceUnit.find_by(id: unit_id.to_s, user_id: user_id.to_s)
168
+ rec ? unit_from_record(rec) : nil
169
+ end
170
+
171
+ def open_episode(_user_id, _episode_key)
172
+ nil
173
+ end
174
+
175
+ def set_open_episode(_user_id, _episode_key, _unit_id)
176
+ nil
177
+ end
178
+
179
+ def close_episode(_unit_id)
180
+ nil
181
+ end
182
+
183
+ def upsert_entity(_entity)
184
+ nil
185
+ end
186
+
187
+ def write_mention(_mention)
188
+ nil
189
+ end
190
+
191
+ def entity_exists?(_user_id, _normalized_key)
192
+ false
193
+ end
194
+
195
+ def trace_ids_for_entity_key(_user_id, _normalized_key)
196
+ []
197
+ end
198
+
199
+ def entity_keys_for_trace(_user_id, _trace_id)
200
+ []
201
+ end
202
+
203
+ def previous_trace(user_id, session_id, sequence)
204
+ list_traces(user_id, session_id: session_id).reverse.find { |t| t.sequence < sequence.to_i }
205
+ end
206
+
207
+ def link_adjacent_traces(_user_id, _prev_trace_id, _trace_id)
208
+ nil
209
+ end
210
+
211
+ def adjacent_traces(_user_id, _trace_id)
212
+ [nil, nil]
213
+ end
214
+
215
+ def store_turn_embedding(_user_id, _trace_id, _vector, model:, dimensions:)
216
+ nil
217
+ end
218
+
219
+ def all_entity_keys(_user_id)
220
+ []
221
+ end
222
+
223
+ def search_traces_by_tokens(user_id, query, limit: 20)
224
+ scope = LlmemoryTrace.where(user_id: user_id.to_s, archived_at: nil)
225
+ tokens = Llmemory::Tokenizer.tokenize(query)
226
+ return [] if tokens.empty?
227
+
228
+ if cipher.enabled? && LlmemoryTrace.column_names.include?("search_tokens")
229
+ digests = tokens.map { |tok| cipher.blind_index(tok) }
230
+ clause = digests.map { "search_tokens LIKE ?" }.join(" OR ")
231
+ scope = scope.where(clause, *digests.map { |d| "% #{d} %" })
232
+ return scope.limit(limit).map { |r| record_to_trace(r) }
233
+ end
234
+
235
+ list_traces(user_id, limit: limit).select do |t|
236
+ down = t.content.downcase
237
+ tokens.all? { |tok| down.include?(tok) }
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ def cipher
244
+ @cipher
245
+ end
246
+
247
+ def metadata_column(metadata)
248
+ meta = metadata || {}
249
+ cipher.enabled? ? enc_json(meta) : meta
250
+ end
251
+
252
+ def decode_metadata(value)
253
+ return {} if value.nil?
254
+ return dec_json(value) if cipher.enabled? && value.is_a?(String)
255
+
256
+ value.is_a?(Hash) ? value.transform_keys(&:to_sym) : {}
257
+ end
258
+
259
+ def record_to_trace(rec)
260
+ Trace.new(
261
+ id: rec.id,
262
+ user_id: rec.user_id,
263
+ session_id: rec.session_id,
264
+ boundary_id: rec.boundary_id,
265
+ sequence: rec.sequence,
266
+ role: rec.role.to_sym,
267
+ content: dec(rec.content),
268
+ occurred_at: rec.occurred_at,
269
+ ingested_at: rec.ingested_at,
270
+ metadata: decode_metadata(rec.metadata),
271
+ content_sha256: rec.content_sha256,
272
+ idempotency_key: rec.idempotency_key,
273
+ archived_at: rec.archived_at
274
+ )
275
+ end
276
+
277
+ def record_to_state_link(rec)
278
+ TraceStateLink.new(
279
+ user_id: rec.user_id,
280
+ state_key: rec.state_key,
281
+ trace_id: rec.trace_id,
282
+ valid_from: rec.valid_from,
283
+ valid_to: rec.valid_to,
284
+ supersedes_trace_id: rec.supersedes_trace_id,
285
+ source: rec.source.to_sym,
286
+ created_at: rec.created_at
287
+ )
288
+ end
289
+
290
+ def unit_from_record(rec)
291
+ data = rec.data.is_a?(Hash) ? rec.data : dec_json(rec.data)
292
+ data = data.transform_keys(&:to_sym)
293
+ TraceUnit.new(
294
+ id: data[:id] || rec.id,
295
+ user_id: data[:user_id] || rec.user_id,
296
+ kind: (data[:kind] || rec.unit_type).to_sym,
297
+ session_id: data[:session_id],
298
+ boundary_id: data[:boundary_id],
299
+ start_sequence: data[:start_sequence],
300
+ end_sequence: data[:end_sequence],
301
+ occurred_from: Time.parse(data[:occurred_from].to_s),
302
+ occurred_to: Time.parse(data[:occurred_to].to_s),
303
+ member_trace_ids: data[:member_trace_ids] || [],
304
+ embedding_ref: data[:embedding_ref],
305
+ index_version: data[:index_version] || 0
306
+ )
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module ZeroMem
5
+ module Storages
6
+ class LlmemoryTrace < ::ActiveRecord::Base
7
+ self.table_name = "llmemory_traces"
8
+ self.primary_key = "id"
9
+ end
10
+
11
+ class LlmemoryTraceStateLink < ::ActiveRecord::Base
12
+ self.table_name = "llmemory_trace_state_links"
13
+ end
14
+
15
+ class LlmemoryTraceUnit < ::ActiveRecord::Base
16
+ self.table_name = "llmemory_trace_units"
17
+ self.primary_key = "id"
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require_relative "../storage"
6
+ require_relative "memory"
7
+ require_relative "snapshot_codec"
8
+ require_relative "../../crypto/field_helpers"
9
+
10
+ module Llmemory
11
+ module ZeroMem
12
+ module Storages
13
+ class FileStorage
14
+ include Storage
15
+ include Llmemory::Crypto::FieldHelpers
16
+
17
+ MUTATORS = %i[
18
+ write_trace archive_trace write_state_link close_open_state_links set_watermark
19
+ write_unit set_open_episode close_episode upsert_entity write_mention
20
+ link_adjacent_traces store_turn_embedding
21
+ ].freeze
22
+
23
+ def initialize(base_path: nil, cipher: nil)
24
+ @base_path = File.expand_path(base_path || Llmemory.configuration.long_term_storage_path || "./llmemory_data")
25
+ @cipher = cipher || Llmemory.build_cipher
26
+ @inner = Memory.new
27
+ load!
28
+ end
29
+
30
+ Storage.instance_methods(false).each do |method_name|
31
+ next if MUTATORS.include?(method_name)
32
+
33
+ define_method(method_name) do |*args, **kwargs, &block|
34
+ @inner.public_send(method_name, *args, **kwargs, &block)
35
+ end
36
+ end
37
+
38
+ MUTATORS.each do |method_name|
39
+ define_method(method_name) do |*args, **kwargs, &block|
40
+ result = @inner.public_send(method_name, *args, **kwargs, &block)
41
+ save!
42
+ result
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def snapshot_path
49
+ File.join(@base_path, "zero_mem", "snapshot.json")
50
+ end
51
+
52
+ def load!
53
+ path = snapshot_path
54
+ return unless File.file?(path)
55
+
56
+ data = read_encrypted_file(path)
57
+ SnapshotCodec.load!(@inner, data)
58
+ end
59
+
60
+ def save!
61
+ FileUtils.mkdir_p(File.dirname(snapshot_path))
62
+ write_encrypted_file(snapshot_path, SnapshotCodec.dump(@inner))
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end