llmemory 0.2.1 → 0.2.3

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 +78 -1
  3. data/app/controllers/llmemory/dashboard/application_controller.rb +15 -1
  4. data/app/controllers/llmemory/dashboard/episodic_controller.rb +22 -0
  5. data/app/controllers/llmemory/dashboard/forget_log_controller.rb +12 -0
  6. data/app/controllers/llmemory/dashboard/maintenance_controller.rb +92 -0
  7. data/app/controllers/llmemory/dashboard/procedural_controller.rb +22 -0
  8. data/app/controllers/llmemory/dashboard/reflection_controller.rb +37 -0
  9. data/app/controllers/llmemory/dashboard/working_controller.rb +14 -0
  10. data/app/views/llmemory/dashboard/episodic/index.html.erb +37 -0
  11. data/app/views/llmemory/dashboard/forget_log/show.html.erb +23 -0
  12. data/app/views/llmemory/dashboard/maintenance/show.html.erb +65 -0
  13. data/app/views/llmemory/dashboard/procedural/index.html.erb +38 -0
  14. data/app/views/llmemory/dashboard/reflection/show.html.erb +29 -0
  15. data/app/views/llmemory/dashboard/users/show.html.erb +16 -0
  16. data/app/views/llmemory/dashboard/working/show.html.erb +20 -0
  17. data/config/routes.rb +14 -0
  18. data/lib/generators/llmemory/install/templates/create_llmemory_tables.rb +2 -0
  19. data/lib/llmemory/cli/commands/maintain.rb +62 -0
  20. data/lib/llmemory/cli/commands/mine_skills.rb +50 -0
  21. data/lib/llmemory/cli.rb +6 -0
  22. data/lib/llmemory/configuration.rb +28 -2
  23. data/lib/llmemory/crypto/cipher.rb +147 -0
  24. data/lib/llmemory/crypto/field_helpers.rb +110 -0
  25. data/lib/llmemory/instrumentation.rb +33 -0
  26. data/lib/llmemory/llm/anthropic.rb +21 -16
  27. data/lib/llmemory/llm/openai.rb +18 -13
  28. data/lib/llmemory/long_term/episodic/memory.rb +27 -13
  29. data/lib/llmemory/long_term/episodic/storage.rb +11 -4
  30. data/lib/llmemory/long_term/episodic/storages/active_record_storage.rb +33 -10
  31. data/lib/llmemory/long_term/episodic/storages/base.rb +15 -2
  32. data/lib/llmemory/long_term/episodic/storages/database_storage.rb +51 -8
  33. data/lib/llmemory/long_term/episodic/storages/file_storage.rb +47 -9
  34. data/lib/llmemory/long_term/episodic/storages/memory_storage.rb +35 -4
  35. data/lib/llmemory/long_term/file_based/memory.rb +12 -4
  36. data/lib/llmemory/long_term/file_based/storage.rb +11 -4
  37. data/lib/llmemory/long_term/file_based/storages/active_record_storage.rb +20 -12
  38. data/lib/llmemory/long_term/file_based/storages/base.rb +2 -2
  39. data/lib/llmemory/long_term/file_based/storages/database_storage.rb +28 -10
  40. data/lib/llmemory/long_term/file_based/storages/file_storage.rb +32 -16
  41. data/lib/llmemory/long_term/file_based/storages/memory_storage.rb +4 -2
  42. data/lib/llmemory/long_term/graph_based/memory.rb +16 -7
  43. data/lib/llmemory/long_term/graph_based/storage.rb +3 -2
  44. data/lib/llmemory/long_term/graph_based/storages/active_record_storage.rb +51 -23
  45. data/lib/llmemory/long_term/graph_based/storages/base.rb +2 -2
  46. data/lib/llmemory/long_term/graph_based/storages/memory_storage.rb +4 -2
  47. data/lib/llmemory/long_term/procedural/memory.rb +30 -16
  48. data/lib/llmemory/long_term/procedural/skill.rb +6 -2
  49. data/lib/llmemory/long_term/procedural/storage.rb +11 -4
  50. data/lib/llmemory/long_term/procedural/storages/active_record_storage.rb +47 -17
  51. data/lib/llmemory/long_term/procedural/storages/base.rb +14 -1
  52. data/lib/llmemory/long_term/procedural/storages/database_storage.rb +52 -10
  53. data/lib/llmemory/long_term/procedural/storages/file_storage.rb +49 -11
  54. data/lib/llmemory/long_term/procedural/storages/memory_storage.rb +36 -5
  55. data/lib/llmemory/maintenance/cognitive_pass.rb +109 -0
  56. data/lib/llmemory/maintenance/ttl_expiry.rb +50 -0
  57. data/lib/llmemory/maintenance.rb +2 -0
  58. data/lib/llmemory/mcp/server.rb +5 -1
  59. data/lib/llmemory/mcp/tools/memory_maintain.rb +53 -0
  60. data/lib/llmemory/mcp/tools/memory_mine_skills.rb +53 -0
  61. data/lib/llmemory/memory.rb +60 -8
  62. data/lib/llmemory/memory_module.rb +13 -6
  63. data/lib/llmemory/reflection/reflector.rb +24 -20
  64. data/lib/llmemory/retrieval/engine.rb +25 -16
  65. data/lib/llmemory/short_term/checkpoint.rb +3 -2
  66. data/lib/llmemory/short_term/stores/active_record_store.rb +12 -10
  67. data/lib/llmemory/short_term/stores/memory_store.rb +1 -1
  68. data/lib/llmemory/short_term/stores/postgres_store.rb +11 -5
  69. data/lib/llmemory/short_term/stores/redis_store.rb +7 -5
  70. data/lib/llmemory/short_term/stores.rb +7 -6
  71. data/lib/llmemory/skill_mining/miner.rb +163 -0
  72. data/lib/llmemory/skill_mining.rb +8 -0
  73. data/lib/llmemory/vector_store/active_record_store.rb +24 -3
  74. data/lib/llmemory/vector_store/memory_store.rb +23 -3
  75. data/lib/llmemory/vector_store/openai_embeddings.rb +11 -7
  76. data/lib/llmemory/vector_store.rb +4 -3
  77. data/lib/llmemory/version.rb +1 -1
  78. data/lib/llmemory.rb +4 -0
  79. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80f259aa60090b95d21110bdf1f91c2c91bd5334a1bc4e3effaec444f241f371
4
- data.tar.gz: 0b07d9a6ce5e485a69f00dfb2ad7c0cd03a442e95817d310a65d85e40f9def2c
3
+ metadata.gz: 8e44ccb1c23fc659d9607e1eb3181e598a57edb95f47b94df4060ad46bfe7c31
4
+ data.tar.gz: 5d80e1fefb1dd77cbdc8c0b4d24e588abaf6f18c570db10e3fc0b2c808b09461
5
5
  SHA512:
6
- metadata.gz: 464cc0765650869996ff24a4bf7e2b3ce4d3433a6c73cc9d63167173b04d14deedf0e169ca4579a7d7ac253b5a858f2b88629ce1b888bf48becfa46fad581979
7
- data.tar.gz: 7624c8cd607cf882b13d500ee878b5412f49e13282014a33dc836cfa68c9bfdf9f759b89f53b51eccb38c32966f0cc66ae5d23a2297f2c965a67d81ccb8f437f
6
+ metadata.gz: edb2f849efc9a6d1dbabc0be0a1210bf21e77f33174b965e3b22e9df3ca90aa536e47152c75891e548d6b43a08d82cfda29afa59d3b2e777aa144f4a3900570f
7
+ data.tar.gz: a8c84411d9c262dee5b5a0c07835c05816283a17749c1b6b282c75694dc56a497d8fb9f2b18eeccd242a82d5dce36234342ede75a13e2739ad11f99fb23ae005
data/README.md CHANGED
@@ -65,6 +65,12 @@ Llmemory.configure do |config|
65
65
  config.long_term_store = :memory # or :file, :postgres, :active_record
66
66
  config.long_term_storage_path = "./llmemory_data" # for :file
67
67
  config.database_url = ENV["DATABASE_URL"] # for :postgres
68
+
69
+ # Optional encryption at rest (AES-256-GCM). Requires a key; isolates data
70
+ # cryptographically per key (e.g. per agent/user). See "Encryption at rest".
71
+ config.encryption_enabled = false
72
+ config.encryption_key = ENV["LLMEMORY_ENCRYPTION_KEY"]
73
+
68
74
  config.time_decay_half_life_days = 30
69
75
  config.max_retrieval_tokens = 2000
70
76
  config.prune_after_days = 90
@@ -112,6 +118,31 @@ Llmemory.configure do |config|
112
118
  end
113
119
  ```
114
120
 
121
+ ## Encryption at rest
122
+
123
+ Optional AES-256-GCM encryption protects persisted memory. Without the key, stored data is unreadable — useful for isolating agents or tenants.
124
+
125
+ ```ruby
126
+ # Global default key (applies to all Memory instances)
127
+ Llmemory.configure do |config|
128
+ config.encryption_enabled = true
129
+ config.encryption_key = ENV["LLMEMORY_ENCRYPTION_KEY"]
130
+ end
131
+
132
+ memory = Llmemory::Memory.new(user_id: "agent-1")
133
+
134
+ # Per-instance key override (isolates this agent even if global config differs)
135
+ memory = Llmemory::Memory.new(user_id: "agent-1", encryption_key: "tenant-specific-secret")
136
+ ```
137
+
138
+ **What is encrypted:** conversation checkpoints (redis/postgres/active_record), file-based facts/resources/categories, episodic/procedural documents, graph node names/types/predicates (deterministic) and properties (random IV). **Vector embeddings are not encrypted** (required for pgvector search); associated `text_content` metadata is encrypted.
139
+
140
+ **Trade-offs:**
141
+ - Database keyword search (`LIKE`, BM25 on encrypted columns) no longer works on ciphertext; file backends still search in memory after decrypt.
142
+ - `:memory` backends are in-process only and are **not** encrypted at rest.
143
+ - Existing plaintext data remains readable (markers `enc:v1:` / `encd:v1:`); new writes are encrypted when enabled.
144
+ - Deterministic encryption on graph identifiers leaks equality (same name ⇒ same ciphertext) but keeps graph traversal working.
145
+
115
146
  ## Long-Term Storage
116
147
 
117
148
  Long-term memory can use different backends:
@@ -216,7 +247,7 @@ llmemory implements the memory and internal-action concepts from [CoALA — Cogn
216
247
  | Procedural memory | `Llmemory::LongTerm::Procedural::Memory` |
217
248
  | Reasoning action | `Llmemory::Actions::Reason` |
218
249
  | Retrieval action | `Retrieval::Engine` (+ feedback, iterative) |
219
- | Learning action | `memorize` / `record_episode` / `register_skill` / reflection |
250
+ | Learning action | `memorize` / `record_episode` / `register_skill` / reflection / skill mining |
220
251
  | Uniform interface | `Llmemory::MemoryModule` (`read`/`write`/`list`/`stats`/`forget`) |
221
252
 
222
253
  All three long-term memories below are **additive** — episodic and procedural coexist with semantic memory rather than replacing it. They support `:memory`, `:file`, `:postgres` and `:active_record` backends. Retrieval is keyword-based by default (tokenized, so multi-word queries work); semantic (embedding) retrieval is **opt-in** via `config.episodic_vector_enabled` / `config.procedural_vector_enabled` (or by injecting a `vector_store:`), which makes `search_candidates` hybrid (vector + keyword).
@@ -310,6 +341,39 @@ skills.find_skill("revert deploy") # best match (a Skill)
310
341
  skills.report_outcome(id, success: true) # feeds ranking + adaptive retrieval
311
342
  ```
312
343
 
344
+ ### Skill mining (episodic → procedural)
345
+
346
+ Rather than writing every skill by hand, mine them from successful episode trajectories (Voyager's contribution: procedural memory grows from lived experience). Mining is **human-in-the-loop by default** — it returns proposals and writes nothing until you opt in.
347
+
348
+ ```ruby
349
+ miner = Llmemory::SkillMining::Miner.new(episodic: episodic, procedural: skills)
350
+
351
+ proposals = miner.mine(window: 20) # => [{ name:, kind:, body:, description:, confidence: }, ...]
352
+ miner.mine(window: 20, outcomes: ["success"]) # deterministic pre-filter by outcome label
353
+ ids = miner.mine(window: 20, auto_register: true) # register proposals straight away
354
+
355
+ # Registered skills carry provenance { method: "skill_mining", sources: [{ type: "episode", id: ... }] }.
356
+ # From the unified API: memory.mine_skills!(auto_register: true)
357
+ ```
358
+
359
+ ### Cognitive maintenance pass (closing the loop)
360
+
361
+ One scheduled step that runs the whole learning loop — consolidate → reflect → mine skills → TTL expiry — for a user. Each step is isolated: a failure is captured in the report and never aborts the others.
362
+
363
+ ```ruby
364
+ report = Llmemory::Maintenance::CognitivePass.run!(
365
+ "u1",
366
+ reflect: true, mine_skills: true, expire: true, # toggle steps
367
+ reflection_window: 10, mining_window: 20
368
+ )
369
+ # => { consolidated:, insights: [...], mined: [...], expired: { episodic:, procedural: }, errors: {} }
370
+
371
+ # From the unified API (wires in the live session, so consolidate! runs too):
372
+ memory.maintain!(mine_skills: true)
373
+ ```
374
+
375
+ `mine_skills` defaults to `config.skill_mining_enabled` (default `false`). Run it from cron or a Rails job. Components (`episodic:`, `procedural:`, `semantic:`, `llm:`) are injectable; built per-config when omitted.
376
+
313
377
  ### Uniform interface (MemoryModule)
314
378
 
315
379
  The queryable long-term memories (file, graph, episodic, procedural) share one agent-facing contract, so a framework can treat them polymorphically:
@@ -489,6 +553,14 @@ llmemory edges USER_ID [--subject NODE_ID] [--limit N]
489
553
  llmemory graph USER_ID [--format dot|json]
490
554
  llmemory search USER_ID "query" [--type short|long|all]
491
555
  llmemory stats [USER_ID]
556
+
557
+ # Cognitive memory (CoALA)
558
+ llmemory episodes USER_ID [--limit N]
559
+ llmemory skills USER_ID [--limit N]
560
+ llmemory working USER_ID SESSION_ID
561
+ llmemory forget-log USER_ID
562
+ llmemory mine-skills USER_ID [--window N] [--outcomes success,recovered] [--register]
563
+ llmemory maintain USER_ID [--[no-]reflect] [--mine-skills] [--[no-]expire] [--window N]
492
564
  ```
493
565
 
494
566
  Use `--store TYPE` where applicable to override the configured store (e.g. `memory`, `redis`, `postgres`, `active_record` for short-term; same or `file` for long-term file-based).
@@ -615,6 +687,11 @@ MCP_TOKEN=your-secret-token llmemory mcp serve --http --port 443 \
615
687
  | `memory_consolidate` | Extract facts from conversation to long-term |
616
688
  | `memory_stats` | Get memory statistics for a user |
617
689
  | `memory_info` | Documentation on how to use the tools |
690
+ | `memory_episode_record` / `memory_episodes` | Record / list episodic trajectories |
691
+ | `memory_skill_register` / `memory_skill_report` / `memory_skills` | Register / outcome-track / list procedural skills |
692
+ | `memory_forget` | Forget entries by id (audited) across any memory type |
693
+ | `memory_mine_skills` | Mine reusable skills from episodes (proposals by default; `auto_register` to save) |
694
+ | `memory_maintain` | Run the cognitive maintenance pass (reflect → mine → expire) and return a report |
618
695
 
619
696
  ### Configuration for Claude Code
620
697
 
@@ -3,7 +3,9 @@
3
3
  module Llmemory
4
4
  module Dashboard
5
5
  class ApplicationController < ActionController::Base
6
- helper_method :short_term_store, :file_based_storage, :graph_based_storage, :file_based?, :graph_based?
6
+ helper_method :short_term_store, :file_based_storage, :graph_based_storage,
7
+ :episodic_storage, :procedural_storage, :forget_log,
8
+ :file_based?, :graph_based?
7
9
 
8
10
  protected
9
11
 
@@ -19,6 +21,18 @@ module Llmemory
19
21
  @graph_based_storage ||= build_graph_based_storage
20
22
  end
21
23
 
24
+ def episodic_storage
25
+ @episodic_storage ||= Llmemory::LongTerm::Episodic::Storages.build
26
+ end
27
+
28
+ def procedural_storage
29
+ @procedural_storage ||= Llmemory::LongTerm::Procedural::Storages.build
30
+ end
31
+
32
+ def forget_log
33
+ @forget_log ||= Llmemory::ForgetLog.new
34
+ end
35
+
22
36
  def long_term_type
23
37
  Llmemory.configuration.long_term_type.to_s
24
38
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class EpisodicController < ApplicationController
6
+ def index
7
+ @user_id = params[:user_id]
8
+ @limit = (params[:limit].presence || 50).to_i
9
+ @offset = (params[:offset].presence || 0).to_i
10
+ @episodes = episodic_storage.list_episodes(@user_id, limit: @limit, offset: @offset)
11
+ @total = episodic_storage.count_episodes(@user_id)
12
+ end
13
+
14
+ def forget
15
+ memory = Llmemory::LongTerm::Episodic::Memory.new(user_id: params[:user_id], storage: episodic_storage)
16
+ mode = params[:mode].to_s == "hard" ? :hard : :soft
17
+ memory.forget(ids: [params[:id]], reason: params[:reason], mode: mode)
18
+ redirect_to user_episodic_path(params[:user_id]), notice: "Forgot episode #{params[:id]} (#{mode})."
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class ForgetLogController < ApplicationController
6
+ def show
7
+ @user_id = params[:user_id]
8
+ @entries = forget_log.entries(@user_id)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,92 @@
1
+ module Llmemory
2
+ module Dashboard
3
+ # Cognitive maintenance surface (SF20): trigger a maintenance pass and review
4
+ # mined-skill proposals before registering them (human-in-the-loop).
5
+ class MaintenanceController < ApplicationController
6
+ def show
7
+ @user_id = params[:user_id]
8
+ @window = (params[:window].presence || 10).to_i
9
+ @recent_episodes = episodic_storage.list_episodes(@user_id, limit: @window)
10
+ @proposals = session_proposals
11
+ end
12
+
13
+ # Runs the full cognitive pass (reflect -> mine -> expire) and reports.
14
+ def run
15
+ user_id = params[:user_id]
16
+ window = (params[:window].presence || 10).to_i
17
+ report = Llmemory::Maintenance::CognitivePass.run!(
18
+ user_id,
19
+ episodic: episodic_memory(user_id),
20
+ procedural: procedural_memory(user_id),
21
+ semantic: build_semantic_memory(user_id),
22
+ mine_skills: params[:mine_skills].present?,
23
+ reflection_window: window
24
+ )
25
+ redirect_to user_maintenance_path(user_id), notice: pass_notice(report)
26
+ rescue Llmemory::LLMError => e
27
+ redirect_to user_maintenance_path(user_id), alert: "Maintenance pass failed: #{e.message}"
28
+ end
29
+
30
+ # Mines skills WITHOUT registering — returns proposals for review.
31
+ def mine
32
+ user_id = params[:user_id]
33
+ window = (params[:window].presence || 20).to_i
34
+ proposals = Llmemory::SkillMining::Miner.new(
35
+ episodic: episodic_memory(user_id), procedural: procedural_memory(user_id)
36
+ ).mine(window: window, auto_register: false)
37
+ store_proposals(proposals)
38
+ redirect_to user_maintenance_path(user_id), notice: "Mined #{proposals.size} skill proposal(s) for review."
39
+ rescue Llmemory::LLMError => e
40
+ redirect_to user_maintenance_path(user_id), alert: "Skill mining failed: #{e.message}"
41
+ end
42
+
43
+ # Registers a single reviewed proposal into procedural memory.
44
+ def register
45
+ user_id = params[:user_id]
46
+ procedural_memory(user_id).register_skill(
47
+ name: params[:name], body: params[:body], description: params[:description].presence,
48
+ kind: params[:kind].presence || Llmemory::LongTerm::Procedural::Skill::DEFAULT_KIND,
49
+ provenance: Llmemory::Provenance.build(method: "skill_mining")
50
+ )
51
+ redirect_to user_maintenance_path(user_id), notice: "Registered skill #{params[:name]}."
52
+ end
53
+
54
+ private
55
+
56
+ def episodic_memory(user_id)
57
+ Llmemory::LongTerm::Episodic::Memory.new(user_id: user_id, storage: episodic_storage)
58
+ end
59
+
60
+ def procedural_memory(user_id)
61
+ Llmemory::LongTerm::Procedural::Memory.new(user_id: user_id, storage: procedural_storage)
62
+ end
63
+
64
+ def build_semantic_memory(user_id)
65
+ if graph_based?
66
+ Llmemory::LongTerm::GraphBased::Memory.new(user_id: user_id, storage: graph_based_storage)
67
+ else
68
+ Llmemory::LongTerm::FileBased::Memory.new(user_id: user_id, storage: file_based_storage)
69
+ end
70
+ end
71
+
72
+ def pass_notice(report)
73
+ expired = report[:expired] || {}
74
+ msg = "Pass complete: #{Array(report[:insights]).size} insight(s), " \
75
+ "#{Array(report[:mined]).size} skill(s) mined, " \
76
+ "expired episodic=#{expired[:episodic] || 0} procedural=#{expired[:procedural] || 0}."
77
+ errors = report[:errors] || {}
78
+ errors.empty? ? msg : "#{msg} Errors: #{errors.map { |k, v| "#{k}: #{v}" }.join('; ')}"
79
+ end
80
+
81
+ # Proposals are stashed in the session so they survive the redirect to
82
+ # `show` without being persisted to a store before the user confirms.
83
+ def store_proposals(proposals)
84
+ session[:llmemory_skill_proposals] = proposals
85
+ end
86
+
87
+ def session_proposals
88
+ Array(session.delete(:llmemory_skill_proposals)).map { |p| p.respond_to?(:symbolize_keys) ? p.symbolize_keys : p }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class ProceduralController < ApplicationController
6
+ def index
7
+ @user_id = params[:user_id]
8
+ @limit = (params[:limit].presence || 50).to_i
9
+ @offset = (params[:offset].presence || 0).to_i
10
+ @skills = procedural_storage.list_skills(@user_id, limit: @limit, offset: @offset)
11
+ @total = procedural_storage.count_skills(@user_id)
12
+ end
13
+
14
+ def forget
15
+ memory = Llmemory::LongTerm::Procedural::Memory.new(user_id: params[:user_id], storage: procedural_storage)
16
+ mode = params[:mode].to_s == "hard" ? :hard : :soft
17
+ memory.forget(ids: [params[:id]], reason: params[:reason], mode: mode)
18
+ redirect_to user_procedural_path(params[:user_id]), notice: "Forgot skill #{params[:id]} (#{mode})."
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ # Read-only landing for reflection. Triggers (POST #run) build a Reflector
6
+ # over the configured episodic + semantic memories and run a single pass
7
+ # over the last N episodes.
8
+ class ReflectionController < ApplicationController
9
+ def show
10
+ @user_id = params[:user_id]
11
+ @recent_window = (params[:window].presence || 10).to_i
12
+ @recent_episodes = episodic_storage.list_episodes(@user_id, limit: @recent_window)
13
+ end
14
+
15
+ def run
16
+ user_id = params[:user_id]
17
+ window = (params[:window].presence || 10).to_i
18
+ episodic = Llmemory::LongTerm::Episodic::Memory.new(user_id: user_id, storage: episodic_storage)
19
+ semantic = build_semantic_memory(user_id)
20
+ insight_ids = Llmemory::Reflection::Reflector.new(episodic: episodic, semantic: semantic).reflect(window: window)
21
+ redirect_to user_reflection_path(user_id), notice: "Reflection produced #{insight_ids.size} insight(s)."
22
+ rescue Llmemory::LLMError => e
23
+ redirect_to user_reflection_path(user_id), alert: "Reflection failed: #{e.message}"
24
+ end
25
+
26
+ private
27
+
28
+ def build_semantic_memory(user_id)
29
+ if graph_based?
30
+ Llmemory::LongTerm::GraphBased::Memory.new(user_id: user_id, storage: graph_based_storage)
31
+ else
32
+ Llmemory::LongTerm::FileBased::Memory.new(user_id: user_id, storage: file_based_storage)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Llmemory
4
+ module Dashboard
5
+ class WorkingController < ApplicationController
6
+ def show
7
+ @user_id = params[:user_id]
8
+ @session_id = params[:session_id]
9
+ @working = Llmemory::WorkingMemory.new(user_id: @user_id, session_id: @session_id, store: short_term_store)
10
+ @slots = @working.to_h
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ <div class="card">
2
+ <h2>Episodic memory: <%= @user_id %></h2>
3
+ <p>Total active episodes: <strong><%= @total %></strong></p>
4
+ <% if @episodes.blank? %>
5
+ <p class="empty">No episodes.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr><th>ID</th><th>Summary</th><th>Outcome</th><th>Steps</th><th>Created</th><th>Forget</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @episodes.each do |e| %>
13
+ <% id = e[:id] || e["id"] %>
14
+ <tr>
15
+ <td><%= id %></td>
16
+ <td><%= truncate((e[:summary] || e["summary"]).to_s, length: 80) %></td>
17
+ <td><%= e[:outcome] || e["outcome"] %></td>
18
+ <td><%= Array(e[:steps] || e["steps"]).size %></td>
19
+ <td><%= e[:created_at] || e["created_at"] %></td>
20
+ <td>
21
+ <%= button_to "Soft", forget_user_episodic_path(@user_id), params: { id: id, mode: "soft" }, method: :post %>
22
+ <%= button_to "Hard", forget_user_episodic_path(@user_id), params: { id: id, mode: "hard" }, method: :post, data: { confirm: "Hard-delete is irreversible." } %>
23
+ </td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ <p>
29
+ <% if @offset.positive? %>
30
+ <a href="?offset=<%= [@offset - @limit, 0].max %>&limit=<%= @limit %>">&laquo; Newer</a>
31
+ <% end %>
32
+ <% if (@offset + @limit) < @total %>
33
+ <a href="?offset=<%= @offset + @limit %>&limit=<%= @limit %>">Older &raquo;</a>
34
+ <% end %>
35
+ </p>
36
+ <% end %>
37
+ </div>
@@ -0,0 +1,23 @@
1
+ <div class="card">
2
+ <h2>Forget log: <%= @user_id %></h2>
3
+ <p>Audit trail of removals (soft-archive + hard-delete). Recorded by <code>MemoryModule#forget</code>.</p>
4
+ <% if @entries.blank? %>
5
+ <p class="empty">No forget events.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr><th>Time</th><th>Memory type</th><th>Ids</th><th>Reason</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @entries.each do |entry| %>
13
+ <tr>
14
+ <td><%= entry[:archived_at] || entry["archived_at"] %></td>
15
+ <td><%= entry[:memory_type] || entry["memory_type"] %></td>
16
+ <td><code><%= Array(entry[:ids] || entry["ids"]).join(", ") %></code></td>
17
+ <td><%= entry[:reason] || entry["reason"] %></td>
18
+ </tr>
19
+ <% end %>
20
+ </tbody>
21
+ </table>
22
+ <% end %>
23
+ </div>
@@ -0,0 +1,65 @@
1
+ <div class="card">
2
+ <h2>Cognitive maintenance: <%= @user_id %></h2>
3
+ <p>The maintenance pass closes the CoALA learning loop: reflect (episodes &rarr; insights), mine skills (episodes &rarr; procedural), and expire entries past their TTL. Each step is isolated &mdash; a failure in one does not abort the others.</p>
4
+
5
+ <%= form_with url: run_user_maintenance_path(@user_id), method: :post, local: true do %>
6
+ <label>Reflection window (episodes): <%= number_field_tag :window, @window, min: 1, max: 100 %></label>
7
+ <label><%= check_box_tag :mine_skills, "1", true %> Mine &amp; register skills</label>
8
+ <%= submit_tag "Run maintenance pass" %>
9
+ <% end %>
10
+ </div>
11
+
12
+ <div class="card">
13
+ <h3>Review mined skills (human-in-the-loop)</h3>
14
+ <p>Mine proposals without registering them, then register the ones you want.</p>
15
+
16
+ <%= form_with url: mine_user_maintenance_path(@user_id), method: :post, local: true do %>
17
+ <label>Mining window (episodes): <%= number_field_tag :window, 20, min: 1, max: 100 %></label>
18
+ <%= submit_tag "Mine proposals" %>
19
+ <% end %>
20
+
21
+ <% if @proposals.present? %>
22
+ <table>
23
+ <thead>
24
+ <tr><th>Name</th><th>Kind</th><th>Confidence</th><th>Body</th><th>Register</th></tr>
25
+ </thead>
26
+ <tbody>
27
+ <% @proposals.each do |p| %>
28
+ <tr>
29
+ <td><%= p[:name] %></td>
30
+ <td><%= p[:kind] %></td>
31
+ <td><%= p[:confidence] %></td>
32
+ <td><%= truncate(p[:body].to_s, length: 120) %></td>
33
+ <td>
34
+ <%= button_to "Register", register_user_maintenance_path(@user_id), method: :post, params: {
35
+ name: p[:name], body: p[:body], kind: p[:kind], description: p[:description]
36
+ } %>
37
+ </td>
38
+ </tr>
39
+ <% end %>
40
+ </tbody>
41
+ </table>
42
+ <% end %>
43
+ </div>
44
+
45
+ <div class="card">
46
+ <h3>Last <%= @window %> episodes</h3>
47
+ <% if @recent_episodes.blank? %>
48
+ <p class="empty">No episodes to learn from.</p>
49
+ <% else %>
50
+ <table>
51
+ <thead>
52
+ <tr><th>ID</th><th>Summary</th><th>Outcome</th></tr>
53
+ </thead>
54
+ <tbody>
55
+ <% @recent_episodes.each do |e| %>
56
+ <tr>
57
+ <td><%= e[:id] || e["id"] %></td>
58
+ <td><%= truncate((e[:summary] || e["summary"]).to_s, length: 100) %></td>
59
+ <td><%= e[:outcome] || e["outcome"] %></td>
60
+ </tr>
61
+ <% end %>
62
+ </tbody>
63
+ </table>
64
+ <% end %>
65
+ </div>
@@ -0,0 +1,38 @@
1
+ <div class="card">
2
+ <h2>Procedural memory (skills): <%= @user_id %></h2>
3
+ <p>Total active skills: <strong><%= @total %></strong></p>
4
+ <% if @skills.blank? %>
5
+ <p class="empty">No skills.</p>
6
+ <% else %>
7
+ <table>
8
+ <thead>
9
+ <tr><th>ID</th><th>Name</th><th>Description</th><th>Success</th><th>Failure</th><th>Version</th><th>Forget</th></tr>
10
+ </thead>
11
+ <tbody>
12
+ <% @skills.each do |s| %>
13
+ <% id = s[:id] || s["id"] %>
14
+ <tr>
15
+ <td><%= id %></td>
16
+ <td><%= s[:name] || s["name"] %></td>
17
+ <td><%= truncate((s[:description] || s["description"]).to_s, length: 100) %></td>
18
+ <td><%= (s[:success_count] || s["success_count"]).to_i %></td>
19
+ <td><%= (s[:failure_count] || s["failure_count"]).to_i %></td>
20
+ <td><%= s[:version] || s["version"] %></td>
21
+ <td>
22
+ <%= button_to "Soft", forget_user_procedural_path(@user_id), params: { id: id, mode: "soft" }, method: :post %>
23
+ <%= button_to "Hard", forget_user_procedural_path(@user_id), params: { id: id, mode: "hard" }, method: :post, data: { confirm: "Hard-delete is irreversible." } %>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ </tbody>
28
+ </table>
29
+ <p>
30
+ <% if @offset.positive? %>
31
+ <a href="?offset=<%= [@offset - @limit, 0].max %>&limit=<%= @limit %>">&laquo; Newer</a>
32
+ <% end %>
33
+ <% if (@offset + @limit) < @total %>
34
+ <a href="?offset=<%= @offset + @limit %>&limit=<%= @limit %>">Older &raquo;</a>
35
+ <% end %>
36
+ </p>
37
+ <% end %>
38
+ </div>
@@ -0,0 +1,29 @@
1
+ <div class="card">
2
+ <h2>Reflection: <%= @user_id %></h2>
3
+ <p>The reflector distills the last N episodes into semantic insights (Reflexion / Generative Agents pattern), with provenance to the source episodes.</p>
4
+
5
+ <%= form_with url: run_user_reflection_path(@user_id), method: :post, local: true do %>
6
+ <label>Window (episodes): <%= number_field_tag :window, @recent_window, min: 1, max: 100 %></label>
7
+ <%= submit_tag "Run reflection" %>
8
+ <% end %>
9
+
10
+ <h3>Last <%= @recent_window %> episodes</h3>
11
+ <% if @recent_episodes.blank? %>
12
+ <p class="empty">No episodes to reflect over.</p>
13
+ <% else %>
14
+ <table>
15
+ <thead>
16
+ <tr><th>ID</th><th>Summary</th><th>Outcome</th></tr>
17
+ </thead>
18
+ <tbody>
19
+ <% @recent_episodes.each do |e| %>
20
+ <tr>
21
+ <td><%= e[:id] || e["id"] %></td>
22
+ <td><%= truncate((e[:summary] || e["summary"]).to_s, length: 100) %></td>
23
+ <td><%= e[:outcome] || e["outcome"] %></td>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ <% end %>
29
+ </div>
@@ -10,5 +10,21 @@
10
10
  <% end %>
11
11
  | <a href="<%= user_stats_path(@user_id) %>">Stats</a>
12
12
  </p>
13
+ <p>
14
+ <strong>Cognitive memory:</strong>
15
+ <a href="<%= user_episodic_path(@user_id) %>">Episodes</a>
16
+ | <a href="<%= user_procedural_path(@user_id) %>">Skills</a>
17
+ | <a href="<%= user_reflection_path(@user_id) %>">Reflection</a>
18
+ | <a href="<%= user_maintenance_path(@user_id) %>">Maintenance</a>
19
+ | <a href="<%= user_forget_log_path(@user_id) %>">Forget log</a>
20
+ </p>
13
21
  <p>Sessions: <%= @sessions.join(", ") %></p>
22
+ <% if @sessions.any? %>
23
+ <p>
24
+ <strong>Working memory:</strong>
25
+ <% @sessions.each do |s| %>
26
+ <a href="<%= user_working_path(@user_id, s) %>"><%= s %></a>
27
+ <% end %>
28
+ </p>
29
+ <% end %>
14
30
  </div>
@@ -0,0 +1,20 @@
1
+ <div class="card">
2
+ <h2>Working memory: <%= @user_id %> / <%= @session_id %></h2>
3
+ <% if @slots.blank? %>
4
+ <p class="empty">No slots set.</p>
5
+ <% else %>
6
+ <table>
7
+ <thead>
8
+ <tr><th>Slot</th><th>Value</th></tr>
9
+ </thead>
10
+ <tbody>
11
+ <% @slots.each do |slot, value| %>
12
+ <tr>
13
+ <td><code><%= slot %></code></td>
14
+ <td><pre><%= value.is_a?(String) ? value : value.inspect %></pre></td>
15
+ </tr>
16
+ <% end %>
17
+ </tbody>
18
+ </table>
19
+ <% end %>
20
+ </div>
data/config/routes.rb CHANGED
@@ -9,4 +9,18 @@ Llmemory::Dashboard::Engine.routes.draw do
9
9
  get "u/:user_id/graph", to: "graph#index", as: :user_graph
10
10
  get "u/:user_id/stats", to: "stats#index", as: :user_stats
11
11
  get "search", to: "search#index", as: :search
12
+
13
+ # Cognitive memory (CoALA) surface
14
+ get "u/:user_id/episodic", to: "episodic#index", as: :user_episodic
15
+ post "u/:user_id/episodic/forget", to: "episodic#forget", as: :forget_user_episodic
16
+ get "u/:user_id/procedural", to: "procedural#index", as: :user_procedural
17
+ post "u/:user_id/procedural/forget", to: "procedural#forget", as: :forget_user_procedural
18
+ get "u/:user_id/working/:session_id", to: "working#show", as: :user_working
19
+ get "u/:user_id/reflection", to: "reflection#show", as: :user_reflection
20
+ post "u/:user_id/reflection/run", to: "reflection#run", as: :run_user_reflection
21
+ get "u/:user_id/forget_log", to: "forget_log#show", as: :user_forget_log
22
+ get "u/:user_id/maintenance", to: "maintenance#show", as: :user_maintenance
23
+ post "u/:user_id/maintenance/run", to: "maintenance#run", as: :run_user_maintenance
24
+ post "u/:user_id/maintenance/mine", to: "maintenance#mine", as: :mine_user_maintenance
25
+ post "u/:user_id/maintenance/register", to: "maintenance#register", as: :register_user_maintenance
12
26
  end
@@ -36,6 +36,7 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
36
36
  t.string :user_id, null: false
37
37
  t.jsonb :data, null: false, default: {}
38
38
  t.text :search_text
39
+ t.datetime :archived_at
39
40
  t.timestamps
40
41
  end
41
42
  add_index :llmemory_episodes, :user_id
@@ -46,6 +47,7 @@ class CreateLlmemoryTables < ActiveRecord::Migration[7.0]
46
47
  t.string :user_id, null: false
47
48
  t.jsonb :data, null: false, default: {}
48
49
  t.text :search_text
50
+ t.datetime :archived_at
49
51
  t.timestamps
50
52
  end
51
53
  add_index :llmemory_skills, :user_id