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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cc4e7d547ef03649a17696bda31fd75bf9ac2062f70bde092e9e2fd0f084c3e
4
- data.tar.gz: 4468f29da8808641c04145637876909e75a077fb3b5186b47b8e488354cb8d5b
3
+ metadata.gz: 004e6d5f30c9eba38319382257934d0644635fd63555e854043f32d7ec4aa7b0
4
+ data.tar.gz: 9295714e79963b31a2c23eb6834a4c31b75c2d83a044ecce905726c5ad8a41b1
5
5
  SHA512:
6
- metadata.gz: 01372cdc24c73fff3ec18862d52a7a7c7cca43de954ab785693e2473a5bd420b3f5354ec816e40c0f80f561d4b9ef0db3bb5e9851bb3c6d3cac2515aebf880a3
7
- data.tar.gz: a68cc51d1779de9bc4f21b304670e253c3d254d4a3eb1fb2577ba790aca66acd9587b7eda1f22847fea422fe12f0ac08e60069d717e1892c6a1564cb4c61ad3c
6
+ metadata.gz: e56b9d1b8a537b1a134be39cc9e0f563eed3f7e20f0cef23bb8dc12aa7881c30512382e027ed5926f507faf09ee2ca691fc2c489fd16bdbf1e1562c7e5e74ca2
7
+ data.tar.gz: a34a15a7eab1202fe44ebcf85e828c81d99941b1102f88242b8efada40884328a22369dd7860f27449401bd8a123c80cdf02305c9d19b5a0215f1a30ebcd6f31
data/README.md CHANGED
@@ -153,6 +153,54 @@ 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
+
179
+ ## Consolidation policy
180
+
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).
182
+
183
+ ```ruby
184
+ Llmemory.configure do |config|
185
+ config.consolidation_policy = Llmemory::Consolidation::RuleBasedPolicy.new(
186
+ drop_predicates: %w[current_job works_at job_title plan_status has_task],
187
+ drop_categories: %w[work_life]
188
+ )
189
+
190
+ # Optional: prefix volatile memories at retrieval time with "[verify live]"
191
+ config.retrieval_label_volatile = true
192
+ end
193
+ ```
194
+
195
+ Policy decisions per extracted unit: `:keep` (default), `:drop` (not persisted), or `:volatile` (persisted with a volatile marker in provenance/properties).
196
+
197
+ To clean **legacy** data consolidated before a policy was enabled:
198
+
199
+ ```bash
200
+ USER_ID=user_123 LONG_TERM_TYPE=graph_based bundle exec rake llmemory:prune_by_policy
201
+ DRY_RUN=1 bundle exec rake llmemory:prune_by_policy
202
+ ```
203
+
156
204
  ## Encryption at rest
157
205
 
158
206
  Optional AES-256-GCM encryption protects persisted memory. Without the key, stored data is unreadable — useful for isolating agents or tenants.
@@ -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
 
@@ -57,7 +57,28 @@ module Llmemory
57
57
  :encryption_key,
58
58
  :shared_memory_stores,
59
59
  :dashboard_auth,
60
- :dashboard_require_auth
60
+ :dashboard_require_auth,
61
+ :consolidation_policy,
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
61
82
 
62
83
  def initialize
63
84
  @llm_provider = :openai
@@ -116,6 +137,50 @@ module Llmemory
116
137
  @shared_memory_stores = false
117
138
  @dashboard_auth = nil
118
139
  @dashboard_require_auth = nil
140
+ @consolidation_policy = nil
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
180
+ end
181
+
182
+ def consolidation_policy
183
+ @consolidation_policy ||= Consolidation::NullPolicy.new
119
184
  end
120
185
 
121
186
  def dashboard_require_auth?
@@ -138,6 +203,7 @@ module Llmemory
138
203
 
139
204
  def configure
140
205
  yield configuration
206
+ configuration.validate!
141
207
  end
142
208
 
143
209
  def reset_configuration!
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Consolidation
5
+ module Filter
6
+ VOLATILE_PROPERTY = "consolidation_volatile"
7
+ VOLATILE_PROVENANCE_FLAG = "volatile"
8
+
9
+ module_function
10
+
11
+ def policy
12
+ Llmemory.configuration.consolidation_policy || NullPolicy.new
13
+ end
14
+
15
+ # @return [Array<Hash>] relations with :volatile key
16
+ def apply_relations(relations)
17
+ Array(relations).filter_map do |relation|
18
+ next unless relation.is_a?(Hash)
19
+
20
+ unit = Unit.from_relation(relation)
21
+ case policy.decide(unit)
22
+ when :drop
23
+ next
24
+ when :volatile
25
+ relation.merge(volatile: true)
26
+ else
27
+ relation.merge(volatile: false)
28
+ end
29
+ end
30
+ end
31
+
32
+ # @return [Array<Hash>] item hashes with :volatile key
33
+ def apply_items(items, classifications = {})
34
+ Array(items).filter_map do |item|
35
+ content = item.is_a?(Hash) ? (item["content"] || item[:content]).to_s : item.to_s
36
+ category = classifications[content] || classifications[content.to_sym]
37
+ unit = Unit.from_item(item, category: category)
38
+
39
+ case policy.decide(unit)
40
+ when :drop
41
+ next
42
+ when :volatile
43
+ item_hash = item.is_a?(Hash) ? item.dup : {"content" => content}
44
+ item_hash.merge(volatile: true)
45
+ else
46
+ item_hash = item.is_a?(Hash) ? item.dup : {"content" => content}
47
+ item_hash.merge(volatile: false)
48
+ end
49
+ end
50
+ end
51
+
52
+ def volatile_provenance(base_provenance)
53
+ prov = base_provenance.is_a?(Hash) ? base_provenance.dup : {}
54
+ prov[:consolidation] = VOLATILE_PROVENANCE_FLAG
55
+ prov["consolidation"] = VOLATILE_PROVENANCE_FLAG unless prov.key?(:consolidation)
56
+ prov
57
+ end
58
+
59
+ def volatile_properties(base_properties)
60
+ props = base_properties.is_a?(Hash) ? base_properties.dup : {}
61
+ props[VOLATILE_PROPERTY] = true
62
+ props
63
+ end
64
+
65
+ def volatile_marked?(properties_or_provenance)
66
+ hash = properties_or_provenance || {}
67
+ hash[VOLATILE_PROPERTY] == true ||
68
+ hash["consolidation_volatile"] == true ||
69
+ hash[:consolidation] == VOLATILE_PROVENANCE_FLAG ||
70
+ hash["consolidation"] == VOLATILE_PROVENANCE_FLAG
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Consolidation
5
+ # Base policy: all extracted units are kept in long-term memory.
6
+ class Policy
7
+ DECISIONS = %i[keep drop volatile].freeze
8
+
9
+ def decide(_unit)
10
+ :keep
11
+ end
12
+ end
13
+
14
+ class NullPolicy < Policy
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Consolidation
5
+ # Rule-based consolidation policy for agents with authoritative live data sources
6
+ # (tools, DB snapshots). Drops or marks volatile facts that should not be treated
7
+ # as stable long-term memory.
8
+ #
9
+ # Precedence (first match wins):
10
+ # 1. drop_if callable
11
+ # 2. volatile_if callable
12
+ # 3. drop_predicates (graph relations)
13
+ # 4. volatile_predicates (graph relations)
14
+ # 5. drop_categories (file-based items)
15
+ # 6. volatile_categories (file-based items)
16
+ # 7. :keep
17
+ class RuleBasedPolicy < Policy
18
+ def initialize(
19
+ drop_predicates: [],
20
+ volatile_predicates: [],
21
+ drop_categories: [],
22
+ volatile_categories: [],
23
+ drop_if: nil,
24
+ volatile_if: nil
25
+ )
26
+ @drop_predicates = normalize_list(drop_predicates)
27
+ @volatile_predicates = normalize_list(volatile_predicates)
28
+ @drop_categories = normalize_list(drop_categories)
29
+ @volatile_categories = normalize_list(volatile_categories)
30
+ @drop_if = drop_if
31
+ @volatile_if = volatile_if
32
+ end
33
+
34
+ def decide(unit)
35
+ return :drop if callable_true?(@drop_if, unit)
36
+ return :volatile if callable_true?(@volatile_if, unit)
37
+
38
+ case unit.kind
39
+ when :relation
40
+ predicate = Unit.normalize_predicate(unit.predicate)
41
+ return :drop if @drop_predicates.include?(predicate)
42
+ return :volatile if @volatile_predicates.include?(predicate)
43
+ when :item
44
+ category = Unit.normalize_category(unit.category)
45
+ return :drop if @drop_categories.include?(category)
46
+ return :volatile if @volatile_categories.include?(category)
47
+ end
48
+
49
+ :keep
50
+ end
51
+
52
+ private
53
+
54
+ def normalize_list(values)
55
+ Array(values).map { |value| value.to_s.strip.downcase.gsub(/\s+/, "_") }.reject(&:empty?)
56
+ end
57
+
58
+ def callable_true?(callable, unit)
59
+ callable.respond_to?(:call) && callable.call(unit) == true
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Consolidation
5
+ # Normalized fact unit passed to Consolidation::Policy.
6
+ # kind: :relation (graph) | :item (file-based)
7
+ Unit = Data.define(:kind, :subject, :predicate, :object, :content, :category, :importance) do
8
+ def self.from_relation(relation)
9
+ new(
10
+ kind: :relation,
11
+ subject: normalize_string(relation[:subject] || relation["subject"]),
12
+ predicate: normalize_predicate(relation[:predicate] || relation["predicate"]),
13
+ object: normalize_string(relation[:object] || relation["object"]),
14
+ content: nil,
15
+ category: nil,
16
+ importance: nil
17
+ )
18
+ end
19
+
20
+ def self.from_item(item, category: nil)
21
+ content = if item.is_a?(Hash)
22
+ (item["content"] || item[:content]).to_s
23
+ else
24
+ item.to_s
25
+ end
26
+ importance = if item.is_a?(Hash)
27
+ item["importance"] || item[:importance]
28
+ end
29
+
30
+ new(
31
+ kind: :item,
32
+ subject: nil,
33
+ predicate: nil,
34
+ object: nil,
35
+ content: content,
36
+ category: normalize_category(category),
37
+ importance: importance
38
+ )
39
+ end
40
+
41
+ def self.normalize_predicate(value)
42
+ normalize_string(value).downcase.gsub(/\s+/, "_")
43
+ end
44
+
45
+ def self.normalize_category(value)
46
+ normalize_string(value).downcase.gsub(/\s+/, "_")
47
+ end
48
+
49
+ def self.normalize_string(value)
50
+ value.to_s.strip
51
+ end
52
+ end
53
+ end
54
+ end