llmemory 0.2.11 → 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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +23 -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 +59 -1
  9. data/lib/llmemory/instrumentation.rb +10 -1
  10. data/lib/llmemory/maintenance/cognitive_pass.rb +29 -1
  11. data/lib/llmemory/maintenance/zero_mem_ttl.rb +26 -0
  12. data/lib/llmemory/maintenance.rb +1 -0
  13. data/lib/llmemory/mcp/server.rb +2 -0
  14. data/lib/llmemory/mcp/store_helpers.rb +16 -0
  15. data/lib/llmemory/mcp/tools/memory_add_message.rb +17 -4
  16. data/lib/llmemory/mcp/tools/memory_consolidate.rb +9 -4
  17. data/lib/llmemory/mcp/tools/memory_maintain.rb +6 -1
  18. data/lib/llmemory/mcp/tools/memory_retrieve.rb +3 -1
  19. data/lib/llmemory/mcp/tools/memory_retrieve_evidence.rb +70 -0
  20. data/lib/llmemory/mcp/tools/memory_stats.rb +22 -0
  21. data/lib/llmemory/memory.rb +257 -9
  22. data/lib/llmemory/retrieval/bm25_scorer.rb +5 -0
  23. data/lib/llmemory/tokenizer.rb +18 -8
  24. data/lib/llmemory/vector_store/memory_store.rb +23 -1
  25. data/lib/llmemory/vector_store/openai_embeddings.rb +47 -21
  26. data/lib/llmemory/version.rb +1 -1
  27. data/lib/llmemory/zero_mem/answer_calibrator.rb +63 -0
  28. data/lib/llmemory/zero_mem/backfill.rb +43 -0
  29. data/lib/llmemory/zero_mem/budget_assembler.rb +62 -0
  30. data/lib/llmemory/zero_mem/circuit_breaker.rb +37 -0
  31. data/lib/llmemory/zero_mem/embedding_provider.rb +73 -0
  32. data/lib/llmemory/zero_mem/engine.rb +247 -0
  33. data/lib/llmemory/zero_mem/entity.rb +31 -0
  34. data/lib/llmemory/zero_mem/entity_extractor.rb +23 -0
  35. data/lib/llmemory/zero_mem/entity_index.rb +65 -0
  36. data/lib/llmemory/zero_mem/entity_mention.rb +35 -0
  37. data/lib/llmemory/zero_mem/errors.rb +7 -0
  38. data/lib/llmemory/zero_mem/evidence.rb +43 -0
  39. data/lib/llmemory/zero_mem/evidence_calibrator.rb +158 -0
  40. data/lib/llmemory/zero_mem/evidence_closure.rb +98 -0
  41. data/lib/llmemory/zero_mem/evidence_fusion.rb +57 -0
  42. data/lib/llmemory/zero_mem/evidence_set.rb +54 -0
  43. data/lib/llmemory/zero_mem/extractors/heuristic.rb +44 -0
  44. data/lib/llmemory/zero_mem/extractors/http.rb +58 -0
  45. data/lib/llmemory/zero_mem/graph_retriever.rb +107 -0
  46. data/lib/llmemory/zero_mem/hierarchy_builder.rb +123 -0
  47. data/lib/llmemory/zero_mem/hierarchy_retriever.rb +133 -0
  48. data/lib/llmemory/zero_mem/indexer.rb +73 -0
  49. data/lib/llmemory/zero_mem/mode.rb +30 -0
  50. data/lib/llmemory/zero_mem/pagerank.rb +42 -0
  51. data/lib/llmemory/zero_mem/query_profile.rb +48 -0
  52. data/lib/llmemory/zero_mem/query_profiler.rb +138 -0
  53. data/lib/llmemory/zero_mem/repair.rb +31 -0
  54. data/lib/llmemory/zero_mem/router.rb +69 -0
  55. data/lib/llmemory/zero_mem/sidecar_client.rb +82 -0
  56. data/lib/llmemory/zero_mem/storage.rb +124 -0
  57. data/lib/llmemory/zero_mem/storages/active_record.rb +311 -0
  58. data/lib/llmemory/zero_mem/storages/active_record_models.rb +21 -0
  59. data/lib/llmemory/zero_mem/storages/file.rb +67 -0
  60. data/lib/llmemory/zero_mem/storages/memory.rb +294 -0
  61. data/lib/llmemory/zero_mem/storages/postgres.rb +85 -0
  62. data/lib/llmemory/zero_mem/storages/snapshot_codec.rb +180 -0
  63. data/lib/llmemory/zero_mem/storages.rb +43 -0
  64. data/lib/llmemory/zero_mem/trace.rb +100 -0
  65. data/lib/llmemory/zero_mem/trace_state_link.rb +64 -0
  66. data/lib/llmemory/zero_mem/trace_unit.rb +83 -0
  67. data/lib/llmemory/zero_mem.rb +42 -0
  68. data/lib/llmemory.rb +7 -0
  69. data/lib/tasks/llmemory.rake +52 -0
  70. metadata +48 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c52d2f218d44057a93d247a535d06a4f4d0877d3ff67fdc48bf9011886ac9199
4
- data.tar.gz: 1934ecd2e82c9518cbb9806c297c172fa5d3c2baa2c676da81943646309cf4bc
3
+ metadata.gz: 004e6d5f30c9eba38319382257934d0644635fd63555e854043f32d7ec4aa7b0
4
+ data.tar.gz: 9295714e79963b31a2c23eb6834a4c31b75c2d83a044ecce905726c5ad8a41b1
5
5
  SHA512:
6
- metadata.gz: 5879fda1878fe865b8e9391f36270cbb85b74b41461a06f66e2c86f3c33475fb383f98ae6dc06391764ad9ec65055b97b146908d3b96d39cff39e4971aeb134b
7
- data.tar.gz: e7f3f99792aa601b49b6c96f3d960acbf4969b5df6b7341aa8074a46e87ab6a96245752a758902300a8c40b7b2683d5bcc4ba8d0517b978169526647a3d4e64c
6
+ metadata.gz: e56b9d1b8a537b1a134be39cc9e0f563eed3f7e20f0cef23bb8dc12aa7881c30512382e027ed5926f507faf09ee2ca691fc2c489fd16bdbf1e1562c7e5e74ca2
7
+ data.tar.gz: a34a15a7eab1202fe44ebcf85e828c81d99941b1102f88242b8efada40884328a22369dd7860f27449401bd8a123c80cdf02305c9d19b5a0215f1a30ebcd6f31
data/README.md CHANGED
@@ -153,6 +153,29 @@ Llmemory.configure do |config|
153
153
  end
154
154
  ```
155
155
 
156
+ ### Zero-Mem (experimental, opt-in)
157
+
158
+ Zero-Mem keeps **immutable traces** as the source of truth and retrieves evidence with deterministic profiling, hierarchy, and entity-graph ranking — **without generative LLM calls** in memory operations (`memory_mode: :zero_mem`). Default remains `:classic`. This is experimental: it does not reproduce published benchmark numbers.
159
+
160
+ ```ruby
161
+ Llmemory.configure do |config|
162
+ config.memory_mode = :zero_mem # or :hybrid (traces + classic long-term; not zero-token)
163
+ config.zero_mem_trace_store = :memory # or :active_record in Rails apps
164
+ end
165
+
166
+ memory = Llmemory::Memory.new(user_id: "u1", session_id: "s1")
167
+ memory.add_message(role: :user, content: "Project Atlas uses Nimbus")
168
+ context = memory.retrieve("Which database does Atlas use?")
169
+ evidence = memory.retrieve_evidence("Which database does Atlas use?", explain: true)
170
+
171
+ # Explicit generative ops raise in :zero_mem
172
+ # memory.consolidate! # => Llmemory::GenerativeOperationDisabled
173
+ ```
174
+
175
+ CLI: `llmemory zero-mem traces USER`, `status`, `reindex`, `search USER "query"`. MCP: `memory_retrieve` (text, same as before) and `memory_retrieve_evidence` (structured JSON + context).
176
+
177
+ Rollout: enable `zero_mem_shadow_write` while staying on `:classic` for reads; see [docs/zero_mem_limitations.md](docs/zero_mem_limitations.md). Benchmarks: [benchmarks/zero_mem/README.md](benchmarks/zero_mem/README.md).
178
+
156
179
  ## Consolidation policy
157
180
 
158
181
  By default, llmemory consolidates every extracted fact into long-term memory. For agents with **authoritative live data** (tools, DB snapshots), configure a `Consolidation::Policy` to **drop** or **mark volatile** facts that should not be treated as stable memory (e.g. current job, plan status, task lists).
@@ -94,9 +94,80 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
94
94
  t.string :source_type, null: false
95
95
  t.string :source_id, null: false
96
96
  t.vector :embedding, limit: 1536
97
+ t.string :embedding_model
98
+ t.integer :embedding_dimensions
97
99
  t.text :text_content
98
100
  t.timestamps
99
101
  end
100
102
  add_index :llmemory_embeddings, [:user_id, :source_type, :source_id], unique: true
103
+
104
+ create_table :llmemory_traces, id: false do |t|
105
+ t.string :id, null: false, primary_key: true
106
+ t.string :user_id, null: false
107
+ t.string :session_id, null: false
108
+ t.string :boundary_id
109
+ t.bigint :sequence, null: false
110
+ t.string :role, null: false
111
+ t.text :content, null: false
112
+ t.text :search_tokens
113
+ t.string :content_sha256, null: false
114
+ t.datetime :occurred_at, null: false
115
+ t.datetime :ingested_at, null: false
116
+ t.jsonb :metadata, null: false, default: {}
117
+ t.string :idempotency_key
118
+ t.datetime :archived_at
119
+ t.timestamps
120
+ end
121
+ add_index :llmemory_traces, [:user_id, :session_id, :sequence], unique: true
122
+ add_index :llmemory_traces, [:user_id, :idempotency_key], unique: true, where: "idempotency_key IS NOT NULL"
123
+ add_index :llmemory_traces, [:user_id, :session_id, :occurred_at]
124
+ add_index :llmemory_traces, [:user_id, :boundary_id]
125
+ add_index :llmemory_traces, [:user_id, :archived_at]
126
+
127
+ create_table :llmemory_trace_state_links do |t|
128
+ t.string :user_id, null: false
129
+ t.string :state_key, null: false
130
+ t.string :trace_id, null: false
131
+ t.datetime :valid_from, null: false
132
+ t.datetime :valid_to
133
+ t.string :supersedes_trace_id
134
+ t.string :source, null: false, default: "explicit"
135
+ t.datetime :created_at, null: false
136
+ end
137
+ add_index :llmemory_trace_state_links, [:user_id, :state_key, :valid_from, :valid_to]
138
+ add_index :llmemory_trace_state_links, :trace_id
139
+
140
+ create_table :llmemory_trace_units, id: false do |t|
141
+ t.string :id, null: false, primary_key: true
142
+ t.string :user_id, null: false
143
+ t.string :trace_id, null: false
144
+ t.string :unit_type, null: false
145
+ t.jsonb :data, null: false, default: {}
146
+ t.datetime :archived_at
147
+ t.timestamps
148
+ end
149
+ add_index :llmemory_trace_units, [:user_id, :trace_id]
150
+
151
+ create_table :llmemory_trace_entities, id: false do |t|
152
+ t.string :id, null: false, primary_key: true
153
+ t.string :user_id, null: false
154
+ t.string :name, null: false
155
+ t.string :entity_type
156
+ t.datetime :archived_at
157
+ t.timestamps
158
+ end
159
+ add_index :llmemory_trace_entities, [:user_id, :name]
160
+
161
+ create_table :llmemory_entity_mentions, id: false do |t|
162
+ t.string :id, null: false, primary_key: true
163
+ t.string :user_id, null: false
164
+ t.string :trace_id, null: false
165
+ t.string :entity_id, null: false
166
+ t.integer :offset_start
167
+ t.integer :offset_end
168
+ t.datetime :archived_at
169
+ t.timestamps
170
+ end
171
+ add_index :llmemory_entity_mentions, [:user_id, :trace_id]
101
172
  end
102
173
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateLlmemoryZeroMemTables < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :llmemory_traces, id: false do |t|
6
+ t.string :id, null: false, primary_key: true
7
+ t.string :user_id, null: false
8
+ t.string :session_id, null: false
9
+ t.string :boundary_id
10
+ t.bigint :sequence, null: false
11
+ t.string :role, null: false
12
+ t.text :content, null: false
13
+ t.text :search_tokens
14
+ t.string :content_sha256, null: false
15
+ t.datetime :occurred_at, null: false
16
+ t.datetime :ingested_at, null: false
17
+ t.jsonb :metadata, null: false, default: {}
18
+ t.string :idempotency_key
19
+ t.datetime :archived_at
20
+ t.timestamps
21
+ end
22
+ add_index :llmemory_traces, [:user_id, :session_id, :sequence], unique: true, name: "idx_llmemory_traces_user_session_sequence"
23
+ add_index :llmemory_traces, [:user_id, :idempotency_key], unique: true, where: "idempotency_key IS NOT NULL",
24
+ name: "idx_llmemory_traces_user_idempotency"
25
+ add_index :llmemory_traces, [:user_id, :session_id, :occurred_at], name: "idx_llmemory_traces_user_session_occurred"
26
+ add_index :llmemory_traces, [:user_id, :boundary_id], name: "idx_llmemory_traces_user_boundary"
27
+ add_index :llmemory_traces, [:user_id, :archived_at], name: "idx_llmemory_traces_user_archived"
28
+
29
+ create_table :llmemory_trace_state_links do |t|
30
+ t.string :user_id, null: false
31
+ t.string :state_key, null: false
32
+ t.string :trace_id, null: false
33
+ t.datetime :valid_from, null: false
34
+ t.datetime :valid_to
35
+ t.string :supersedes_trace_id
36
+ t.string :source, null: false, default: "explicit"
37
+ t.datetime :created_at, null: false
38
+ end
39
+ add_index :llmemory_trace_state_links, [:user_id, :state_key, :valid_from, :valid_to],
40
+ name: "idx_llmemory_trace_state_links_validity"
41
+ add_index :llmemory_trace_state_links, :trace_id, name: "idx_llmemory_trace_state_links_trace"
42
+
43
+ # Reserved for ZM2/ZM3 — created now to avoid chained migrations later.
44
+ create_table :llmemory_trace_units, id: false do |t|
45
+ t.string :id, null: false, primary_key: true
46
+ t.string :user_id, null: false
47
+ t.string :trace_id, null: false
48
+ t.string :unit_type, null: false
49
+ t.jsonb :data, null: false, default: {}
50
+ t.datetime :archived_at
51
+ t.timestamps
52
+ end
53
+ add_index :llmemory_trace_units, [:user_id, :trace_id], name: "idx_llmemory_trace_units_user_trace"
54
+
55
+ create_table :llmemory_trace_entities, id: false do |t|
56
+ t.string :id, null: false, primary_key: true
57
+ t.string :user_id, null: false
58
+ t.string :name, null: false
59
+ t.string :entity_type
60
+ t.datetime :archived_at
61
+ t.timestamps
62
+ end
63
+ add_index :llmemory_trace_entities, [:user_id, :name], name: "idx_llmemory_trace_entities_user_name"
64
+
65
+ create_table :llmemory_entity_mentions, id: false do |t|
66
+ t.string :id, null: false, primary_key: true
67
+ t.string :user_id, null: false
68
+ t.string :trace_id, null: false
69
+ t.string :entity_id, null: false
70
+ t.integer :offset_start
71
+ t.integer :offset_end
72
+ t.datetime :archived_at
73
+ t.timestamps
74
+ end
75
+ add_index :llmemory_entity_mentions, [:user_id, :trace_id], name: "idx_llmemory_entity_mentions_user_trace"
76
+ end
77
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+
5
+ module Llmemory
6
+ module Generators
7
+ class UpgradeZeroMemGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ desc "Add Zero-Mem trace tables (llmemory_traces, state links, reserved index tables)"
11
+
12
+ def copy_migration
13
+ timestamp = Time.now.utc.strftime("%Y%m%d%H%M%S")
14
+ template "create_llmemory_zero_mem_tables.rb",
15
+ "db/migrate/#{timestamp}_create_llmemory_zero_mem_tables.rb"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "base"
5
+
6
+ module Llmemory
7
+ module Cli
8
+ module Commands
9
+ class ZeroMem < Base
10
+ def option_parser(parser)
11
+ parser.on("--session SESSION", "Session id (default: default)") { |v| @session_id = v }
12
+ parser.on("--explain", "Include explain payload on search") { @explain = true }
13
+ end
14
+
15
+ def execute(argv, _opts)
16
+ sub = argv.shift
17
+ case sub
18
+ when "traces"
19
+ run_traces(argv)
20
+ when "reindex"
21
+ run_reindex(argv)
22
+ when "status"
23
+ run_status(argv)
24
+ when "search"
25
+ run_search(argv)
26
+ else
27
+ print_usage
28
+ exit 1
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def print_usage
35
+ $stderr.puts <<~HELP
36
+ Usage:
37
+ llmemory zero-mem traces USER_ID [--session SESSION]
38
+ llmemory zero-mem reindex USER_ID [--session SESSION]
39
+ llmemory zero-mem status USER_ID [--session SESSION]
40
+ llmemory zero-mem search USER_ID "query" [--explain]
41
+ HELP
42
+ end
43
+
44
+ def build_memory(user_id)
45
+ trace_store = Llmemory::ZeroMem::Storages.build
46
+ Llmemory::Memory.new(
47
+ user_id: user_id,
48
+ session_id: @session_id || "default",
49
+ trace_store: trace_store,
50
+ memory_mode: :zero_mem
51
+ )
52
+ end
53
+
54
+ def run_traces(argv)
55
+ user_id = argv.shift
56
+ unless user_id
57
+ print_usage
58
+ exit 1
59
+ end
60
+
61
+ memory = build_memory(user_id)
62
+ traces = memory.trace_store.list_traces(user_id, session_id: @session_id || "default")
63
+ traces.each do |t|
64
+ puts "[#{t.id}] seq=#{t.sequence} #{t.role}: #{t.content[0, 120]}"
65
+ end
66
+ end
67
+
68
+ def run_reindex(argv)
69
+ user_id = argv.shift
70
+ unless user_id
71
+ print_usage
72
+ exit 1
73
+ end
74
+
75
+ memory = build_memory(user_id)
76
+ status = memory.reindex_traces!(session_id: @session_id || "default")
77
+ puts JSON.generate(status)
78
+ end
79
+
80
+ def run_status(argv)
81
+ user_id = argv.shift
82
+ unless user_id
83
+ print_usage
84
+ exit 1
85
+ end
86
+
87
+ memory = build_memory(user_id)
88
+ puts JSON.generate(memory.zero_mem_status(session_id: @session_id || "default"))
89
+ end
90
+
91
+ def run_search(argv)
92
+ user_id = argv.shift
93
+ query = argv.join(" ").strip
94
+ unless user_id && !query.empty?
95
+ print_usage
96
+ exit 1
97
+ end
98
+
99
+ memory = build_memory(user_id)
100
+ result = memory.retrieve_evidence(query, explain: @explain == true)
101
+ puts result.to_context
102
+ puts JSON.generate(metrics: result.metrics, route: result.route) if @explain
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/llmemory/cli.rb CHANGED
@@ -13,6 +13,7 @@ require_relative "cli/commands/mine_skills"
13
13
  require_relative "cli/commands/maintain"
14
14
  require_relative "cli/commands/stats"
15
15
  require_relative "cli/commands/search"
16
+ require_relative "cli/commands/zero_mem"
16
17
  require_relative "cli/commands/mcp"
17
18
 
18
19
  module Llmemory
@@ -60,6 +61,7 @@ module Llmemory
60
61
  "mine_skills" => Cli::Commands::MineSkills,
61
62
  "maintain" => Cli::Commands::Maintain,
62
63
  "search" => Cli::Commands::Search,
64
+ "zero_mem" => Cli::Commands::ZeroMem,
63
65
  "stats" => Cli::Commands::Stats,
64
66
  "mcp" => Cli::Commands::Mcp
65
67
  }[normalized]
@@ -87,6 +89,7 @@ module Llmemory
87
89
  mine-skills USER_ID Mine reusable skills from episodes (--register to save)
88
90
  maintain USER_ID Run the cognitive maintenance pass (reflect/mine/expire)
89
91
  search USER_ID "query" Search in memory
92
+ zero-mem SUB ... Zero-Mem traces, reindex, status, search (experimental)
90
93
  stats [USER_ID] Show statistics
91
94
  mcp [serve] Start MCP server for LLM agents
92
95
 
@@ -59,7 +59,26 @@ module Llmemory
59
59
  :dashboard_auth,
60
60
  :dashboard_require_auth,
61
61
  :consolidation_policy,
62
- :retrieval_label_volatile
62
+ :retrieval_label_volatile,
63
+ :zero_mem_index_mode,
64
+ :zero_mem_trace_store,
65
+ :zero_mem_top_k,
66
+ :zero_mem_max_top_k,
67
+ :zero_mem_local_span,
68
+ :zero_mem_window_size,
69
+ :zero_mem_window_overlap,
70
+ :zero_mem_episode_gap_seconds,
71
+ :zero_mem_entity_extractor,
72
+ :zero_mem_embedding_provider,
73
+ :zero_mem_require_local_encoders,
74
+ :zero_mem_pagerank_damping,
75
+ :zero_mem_graph_weight,
76
+ :zero_mem_ner_http_url,
77
+ :memory_mode,
78
+ :zero_mem_ttl_days,
79
+ :zero_mem_sidecar_timeout_seconds,
80
+ :zero_mem_sidecar_failure_threshold,
81
+ :zero_mem_shadow_write
63
82
 
64
83
  def initialize
65
84
  @llm_provider = :openai
@@ -120,6 +139,44 @@ module Llmemory
120
139
  @dashboard_require_auth = nil
121
140
  @consolidation_policy = nil
122
141
  @retrieval_label_volatile = false
142
+ @zero_mem_index_mode = :sync
143
+ @zero_mem_trace_store = :memory
144
+ @zero_mem_top_k = 5
145
+ @zero_mem_max_top_k = 10
146
+ @zero_mem_local_span = 2
147
+ @zero_mem_window_size = 8
148
+ @zero_mem_window_overlap = 2
149
+ @zero_mem_episode_gap_seconds = 86_400
150
+ @zero_mem_entity_extractor = :heuristic
151
+ @zero_mem_embedding_provider = nil
152
+ @zero_mem_require_local_encoders = false
153
+ @zero_mem_pagerank_damping = 0.6
154
+ @zero_mem_graph_weight = 0.6
155
+ @zero_mem_ner_http_url = "http://127.0.0.1:8765"
156
+ @memory_mode = :classic
157
+ @zero_mem_ttl_days = nil
158
+ @zero_mem_sidecar_timeout_seconds = 5
159
+ @zero_mem_sidecar_failure_threshold = 3
160
+ @zero_mem_shadow_write = false
161
+ end
162
+
163
+ def validate!
164
+ mode = @memory_mode.to_sym
165
+ unless ZeroMem::Mode.valid?(mode)
166
+ raise ConfigurationError, "memory_mode must be :classic, :zero_mem, or :hybrid (got #{@memory_mode.inspect})"
167
+ end
168
+
169
+ rho = zero_mem_graph_weight.to_f
170
+ gamma = zero_mem_pagerank_damping.to_f
171
+ raise ConfigurationError, "zero_mem_graph_weight must be in [0, 1]" unless rho.between?(0.0, 1.0)
172
+ raise ConfigurationError, "zero_mem_pagerank_damping must be in [0, 1]" unless gamma.between?(0.0, 1.0)
173
+ raise ConfigurationError, "zero_mem_top_k must be positive" unless zero_mem_top_k.to_i.positive?
174
+ raise ConfigurationError, "zero_mem_max_top_k must be >= zero_mem_top_k" if zero_mem_max_top_k.to_i < zero_mem_top_k.to_i
175
+
176
+ extractor = zero_mem_entity_extractor.to_sym
177
+ unless %i[heuristic http].include?(extractor)
178
+ raise ConfigurationError, "zero_mem_entity_extractor must be :heuristic or :http"
179
+ end
123
180
  end
124
181
 
125
182
  def consolidation_policy
@@ -146,6 +203,7 @@ module Llmemory
146
203
 
147
204
  def configure
148
205
  yield configuration
206
+ configuration.validate!
149
207
  end
150
208
 
151
209
  def reset_configuration!
@@ -16,10 +16,19 @@ module Llmemory
16
16
  # output_tokens:, total_tokens:
17
17
  # memory_write.llmemory memory_type:, user_id:
18
18
  # memory_forget.llmemory memory_type:, user_id:, count:
19
- # retrieve.llmemory query_chars:, candidates:, results:
19
+ # retrieve.llmemory query_chars:, candidates:, results:, zero_mem_compliant:
20
20
  # iterative_retrieve.llmemory hops:, total_results:
21
21
  # reflect.llmemory window:, insights:
22
22
  # mine_skills.llmemory window:, auto_register:
23
+ # trace_write.llmemory user_id:, session_id:, trace_id:, sequence:, maintenance_fanout:
24
+ # zero_mem_state_update.llmemory user_id:, state_key:, trace_id:, supersedes_trace_id:
25
+ # zero_mem_profile.llmemory user_id:, query_chars:
26
+ # zero_mem_hierarchy_retrieve.llmemory user_id:, workload:
27
+ # zero_mem_retrieve.llmemory user_id:, evidence_count:, degraded:
28
+ # zero_mem_graph_retrieve.llmemory user_id:
29
+ # zero_mem_closure.llmemory user_id:, seeds:
30
+ # zero_mem_calibrate.llmemory user_id:, trace_count:
31
+ # encoder_embed.llmemory user_id:, trace_id:, model:, dimensions:, duration_ms:
23
32
  module Instrumentation
24
33
  module_function
25
34
 
@@ -43,7 +43,22 @@ module Llmemory
43
43
  end
44
44
 
45
45
  def run!
46
- report = { consolidated: nil, insights: [], mined: [], expired: { episodic: 0, procedural: 0 }, errors: {} }
46
+ report = {
47
+ consolidated: nil,
48
+ insights: [],
49
+ mined: [],
50
+ expired: { episodic: 0, procedural: 0 },
51
+ errors: {},
52
+ disabled: []
53
+ }
54
+
55
+ if @memory&.zero_mem_strict?
56
+ report[:disabled] = %i[consolidate reflect mine]
57
+ report[:consolidated] = false
58
+ step(report, :zero_mem_repair) { report[:zero_mem] = zero_mem_repair } if @memory.trace_store
59
+ step(report, :expire) { report[:expired] = expire } if @expire
60
+ return report
61
+ end
47
62
 
48
63
  step(report, :consolidate) { report[:consolidated] = consolidate } if @memory
49
64
  step(report, :reflect) { report[:insights] = reflect } if @reflect
@@ -79,6 +94,19 @@ module Llmemory
79
94
  TTLExpiry.run!(@user_id, episodic: episodic, procedural: procedural)
80
95
  end
81
96
 
97
+ def zero_mem_repair
98
+ store = @memory.trace_store
99
+ repair = ZeroMem::Repair.new(storage: store)
100
+ sessions = store.list_traces(@user_id).map(&:session_id).uniq
101
+ sessions.map do |sid|
102
+ repair.run!(
103
+ user_id: @user_id,
104
+ session_id: sid,
105
+ checkpoint_messages: @memory.messages
106
+ )
107
+ end
108
+ end
109
+
82
110
  def episodic
83
111
  @episodic ||= @memory&.episodic || Llmemory::LongTerm::Episodic::Memory.new(user_id: @user_id)
84
112
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Maintenance
5
+ class ZeroMemTTL
6
+ def initialize(storage:, ttl_days: nil)
7
+ @storage = storage
8
+ @ttl_days = ttl_days || Llmemory.configuration.zero_mem_ttl_days
9
+ end
10
+
11
+ def run!(user_id:, dry_run: false)
12
+ return { archived: 0, skipped: true } if @ttl_days.nil? || @ttl_days.to_i <= 0
13
+
14
+ cutoff = Time.now - (@ttl_days.to_i * 86_400)
15
+ archived = 0
16
+ @storage.list_traces(user_id, include_archived: false).each do |trace|
17
+ next unless trace.occurred_at < cutoff
18
+
19
+ archived += 1
20
+ @storage.archive_trace(user_id, trace.id) unless dry_run
21
+ end
22
+ { archived: archived, dry_run: dry_run, cutoff: cutoff.utc.iso8601 }
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,6 +5,7 @@ require_relative "maintenance/ttl_expiry"
5
5
  require_relative "maintenance/cognitive_pass"
6
6
  require_relative "maintenance/search_tokens_backfill"
7
7
  require_relative "maintenance/policy_prune"
8
+ require_relative "maintenance/zero_mem_ttl"
8
9
 
9
10
  module Llmemory
10
11
  module Maintenance
@@ -5,6 +5,7 @@ require_relative "authentication"
5
5
  require_relative "tools/memory_search"
6
6
  require_relative "tools/memory_save"
7
7
  require_relative "tools/memory_retrieve"
8
+ require_relative "tools/memory_retrieve_evidence"
8
9
  require_relative "tools/memory_timeline"
9
10
  require_relative "tools/memory_add_message"
10
11
  require_relative "tools/memory_consolidate"
@@ -175,6 +176,7 @@ module Llmemory
175
176
  Tools::MemorySearch,
176
177
  Tools::MemorySave,
177
178
  Tools::MemoryRetrieve,
179
+ Tools::MemoryRetrieveEvidence,
178
180
  Tools::MemoryTimeline,
179
181
  Tools::MemoryTimelineContext,
180
182
  Tools::MemoryAddMessage,
@@ -30,6 +30,22 @@ module Llmemory
30
30
  def graph_based?
31
31
  Llmemory.configuration.long_term_type.to_sym == :graph_based
32
32
  end
33
+
34
+ def trace_store_instance
35
+ @trace_store_instance ||= ZeroMem::Storages.build
36
+ end
37
+
38
+ def build_memory(user_id:, session_id: "default", trace_store: nil, memory_mode: nil)
39
+ mode = memory_mode || Llmemory.configuration.memory_mode
40
+ store = trace_store
41
+ store = trace_store_instance if store.nil? && ZeroMem::Mode.zero_mem_enabled?(mode)
42
+ Llmemory::Memory.new(
43
+ user_id: user_id,
44
+ session_id: session_id,
45
+ trace_store: store,
46
+ memory_mode: mode
47
+ )
48
+ end
33
49
  end
34
50
  end
35
51
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../store_helpers"
4
+
3
5
  module Llmemory
4
6
  module MCP
5
7
  module Tools
@@ -11,17 +13,28 @@ module Llmemory
11
13
  user_id: { type: "string", description: "User identifier" },
12
14
  session_id: { type: "string", description: "Session identifier (default: 'default')" },
13
15
  role: { type: "string", enum: ["user", "assistant", "system", "tool", "tool_result"], description: "Message role" },
14
- content: { type: "string", description: "Message content" }
16
+ content: { type: "string", description: "Message content" },
17
+ occurred_at: { type: "string", description: "Optional ISO8601 timestamp for trace ingestion" },
18
+ boundary_id: { type: "string", description: "Optional boundary identifier (Zero-Mem)" },
19
+ idempotency_key: { type: "string", description: "Optional idempotency key for trace writes" }
15
20
  },
16
21
  required: ["user_id", "role", "content"]
17
22
  )
18
23
 
19
24
  class << self
20
- def call(user_id:, role:, content:, session_id: nil, server_context: nil)
25
+ def call(user_id:, role:, content:, session_id: nil, occurred_at: nil, boundary_id: nil,
26
+ idempotency_key: nil, server_context: nil)
21
27
  session = session_id || "default"
22
28
 
23
- memory = Llmemory::Memory.new(user_id: user_id, session_id: session)
24
- memory.add_message(role: role.to_sym, content: content)
29
+ memory = StoreHelpers.build_memory(user_id: user_id, session_id: session)
30
+ parsed_time = occurred_at ? Time.parse(occurred_at.to_s) : nil
31
+ memory.add_message(
32
+ role: role.to_sym,
33
+ content: content,
34
+ occurred_at: parsed_time,
35
+ boundary_id: boundary_id,
36
+ idempotency_key: idempotency_key
37
+ )
25
38
 
26
39
  ::MCP::Tool::Response.new([{
27
40
  type: "text",
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../store_helpers"
4
+
3
5
  module Llmemory
4
6
  module MCP
5
7
  module Tools
@@ -20,7 +22,7 @@ module Llmemory
20
22
  session = session_id || "default"
21
23
  should_clear = clear_session || false
22
24
 
23
- memory = Llmemory::Memory.new(user_id: user_id, session_id: session)
25
+ memory = StoreHelpers.build_memory(user_id: user_id, session_id: session)
24
26
 
25
27
  messages = memory.messages
26
28
  if messages.empty?
@@ -33,9 +35,7 @@ module Llmemory
33
35
  message_count = messages.size
34
36
  memory.consolidate!
35
37
 
36
- if should_clear
37
- memory.clear_session!
38
- end
38
+ memory.clear_session! if should_clear
39
39
 
40
40
  response_text = "Consolidated #{message_count} messages from session '#{session}' into long-term memory."
41
41
  response_text += "\nSession cleared." if should_clear
@@ -44,6 +44,11 @@ module Llmemory
44
44
  type: "text",
45
45
  text: response_text
46
46
  }])
47
+ rescue Llmemory::GenerativeOperationDisabled => e
48
+ ::MCP::Tool::Response.new([{
49
+ type: "text",
50
+ text: e.message
51
+ }], error: true)
47
52
  rescue => e
48
53
  ::MCP::Tool::Response.new([{
49
54
  type: "text",