Granis87 commited on
Commit
c3a3710
·
verified ·
1 Parent(s): dbb04e4

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env.example +116 -0
  2. .gitignore +1 -0
  3. REFACTORING_TODO.md +8 -5
  4. RELEASE_CHECKLIST.md +43 -65
  5. config.yaml +1 -1
  6. data/mnemocore_hnsw.faiss +0 -0
  7. data/mnemocore_hnsw_idmap.json +1 -0
  8. data/mnemocore_hnsw_vectors.npy +3 -0
  9. data/subconscious_evolution.json +2 -2
  10. docs/AGI_MEMORY_BLUEPRINT.md +713 -0
  11. integrations/README.md +233 -0
  12. integrations/aider/aider_wrap.sh +44 -0
  13. integrations/claude_code/CLAUDE_memory_snippet.md +38 -0
  14. integrations/claude_code/hooks/post_tool_store.py +96 -0
  15. integrations/claude_code/hooks/pre_session_inject.py +93 -0
  16. integrations/claude_code/hooks_config_fragment.json +28 -0
  17. integrations/claude_code/mcp_config.json +13 -0
  18. integrations/gemini_cli/GEMINI_memory_snippet.md +35 -0
  19. integrations/gemini_cli/gemini_wrap.sh +47 -0
  20. integrations/mnemo_bridge.py +177 -0
  21. integrations/setup.ps1 +158 -0
  22. integrations/setup.sh +299 -0
  23. integrations/universal/context_inject.sh +29 -0
  24. integrations/universal/store_session.sh +40 -0
  25. mnemocore_verify.py +136 -0
  26. src/mnemocore/agent_interface.py +145 -0
  27. src/mnemocore/api/main.py +365 -1
  28. src/mnemocore/core/agent_profile.py +65 -0
  29. src/mnemocore/core/anticipatory.py +51 -0
  30. src/mnemocore/core/binary_hdv.py +41 -31
  31. src/mnemocore/core/confidence.py +196 -0
  32. src/mnemocore/core/config.py +91 -11
  33. src/mnemocore/core/container.py +24 -0
  34. src/mnemocore/core/contradiction.py +336 -0
  35. src/mnemocore/core/cross_domain.py +211 -0
  36. src/mnemocore/core/emotional_tag.py +124 -0
  37. src/mnemocore/core/engine.py +230 -55
  38. src/mnemocore/core/episodic_store.py +144 -0
  39. src/mnemocore/core/forgetting_curve.py +233 -0
  40. src/mnemocore/core/hnsw_index.py +170 -140
  41. src/mnemocore/core/memory_model.py +132 -0
  42. src/mnemocore/core/meta_memory.py +70 -0
  43. src/mnemocore/core/node.py +20 -1
  44. src/mnemocore/core/prediction_store.py +294 -0
  45. src/mnemocore/core/preference_store.py +53 -0
  46. src/mnemocore/core/procedural_store.py +77 -0
  47. src/mnemocore/core/provenance.py +297 -0
  48. src/mnemocore/core/pulse.py +110 -0
  49. src/mnemocore/core/qdrant_store.py +38 -16
  50. src/mnemocore/core/semantic_consolidation.py +13 -0
.env.example ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore Environment Configuration
2
+ # ====================================
3
+ # Copy this file to .env and fill in the values.
4
+ # All variables can be overridden at runtime.
5
+
6
+ # ===========================================
7
+ # REQUIRED: API Security
8
+ # ===========================================
9
+ # API key for authentication (REQUIRED - must be set)
10
+ # Generate a secure key: python -c "import secrets; print(secrets.token_urlsafe(32))"
11
+ HAIM_API_KEY=your-secure-api-key-here
12
+
13
+ # ===========================================
14
+ # Redis Configuration
15
+ # ===========================================
16
+ # Redis connection URL
17
+ # Format: redis://[username:password@]host:port/db
18
+ REDIS_URL=redis://redis:6379/0
19
+
20
+ # Redis stream key for pub/sub events
21
+ REDIS_STREAM_KEY=haim:subconscious
22
+
23
+ # Maximum Redis connections
24
+ REDIS_MAX_CONNECTIONS=10
25
+
26
+ # Redis socket timeout (seconds)
27
+ REDIS_SOCKET_TIMEOUT=5
28
+
29
+ # ===========================================
30
+ # Qdrant Configuration
31
+ # ===========================================
32
+ # Qdrant connection URL
33
+ QDRANT_URL=http://qdrant:6333
34
+
35
+ # Collection names
36
+ QDRANT_COLLECTION_HOT=haim_hot
37
+ QDRANT_COLLECTION_WARM=haim_warm
38
+
39
+ # ===========================================
40
+ # Server Configuration
41
+ # ===========================================
42
+ # Host to bind the server
43
+ HOST=0.0.0.0
44
+
45
+ # Port to listen on
46
+ PORT=8100
47
+
48
+ # Number of uvicorn workers (1 recommended for stateful apps)
49
+ WORKERS=1
50
+
51
+ # ===========================================
52
+ # Logging Configuration
53
+ # ===========================================
54
+ # Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
55
+ LOG_LEVEL=INFO
56
+
57
+ # Enable structured JSON logging
58
+ STRUCTURED_LOGGING=true
59
+
60
+ # ===========================================
61
+ # Observability (Prometheus)
62
+ # ===========================================
63
+ # Port for Prometheus metrics
64
+ METRICS_PORT=9090
65
+
66
+ # ===========================================
67
+ # Memory Tier Configuration
68
+ # ===========================================
69
+ # Hot tier max memories
70
+ HOT_MAX_MEMORIES=2000
71
+
72
+ # Warm tier max memories
73
+ WARM_MAX_MEMORIES=100000
74
+
75
+ # LTP decay rate
76
+ LTP_DECAY_LAMBDA=0.01
77
+
78
+ # ===========================================
79
+ # GPU Configuration (Optional)
80
+ # ===========================================
81
+ # Enable GPU acceleration
82
+ GPU_ENABLED=false
83
+
84
+ # CUDA device (e.g., cuda:0)
85
+ GPU_DEVICE=cuda:0
86
+
87
+ # ===========================================
88
+ # MCP Bridge Configuration (Optional)
89
+ # ===========================================
90
+ # Enable MCP bridge
91
+ MCP_ENABLED=false
92
+
93
+ # MCP transport: stdio, tcp
94
+ MCP_TRANSPORT=stdio
95
+
96
+ # MCP host and port (for TCP transport)
97
+ MCP_HOST=127.0.0.1
98
+ MCP_PORT=8110
99
+
100
+ # ===========================================
101
+ # CORS Configuration (Optional)
102
+ # ===========================================
103
+ # Allowed CORS origins (comma-separated)
104
+ # CORS_ORIGINS=http://localhost:3000,https://example.com
105
+
106
+ # ===========================================
107
+ # Rate Limiting (Optional)
108
+ # ===========================================
109
+ # Enable rate limiting
110
+ RATE_LIMIT_ENABLED=true
111
+
112
+ # Requests per window
113
+ RATE_LIMIT_REQUESTS=100
114
+
115
+ # Window size in seconds
116
+ RATE_LIMIT_WINDOW=60
.gitignore CHANGED
@@ -38,6 +38,7 @@ ENV/
38
  htmlcov/
39
  .tox/
40
  .nox/
 
41
 
42
  # Data (runtime generated)
43
  data/memory.jsonl
 
38
  htmlcov/
39
  .tox/
40
  .nox/
41
+ .hypothesis/
42
 
43
  # Data (runtime generated)
44
  data/memory.jsonl
REFACTORING_TODO.md CHANGED
@@ -26,7 +26,7 @@ Status för kodoptimering inför kommande funktionalitet.
26
  ---
27
 
28
  ### 2. Ofullständiga features
29
- **Status:** Pending
30
 
31
  **Problem:**
32
  - Flera TODOs i produktionskod som lämnats oimplementerade
@@ -44,8 +44,10 @@ Line 320: # TODO: orchestrate_orch_or() not implemented
44
  ```
45
 
46
  **Åtgärd:**
47
- - Implementera funktionerna
48
- - Eller ta bort dödkod
 
 
49
 
50
  ---
51
 
@@ -165,7 +167,7 @@ Import-stilen följer redan rekommenderad Python-praxis. Ingen åtgärd behövs.
165
  ## Förbättra testtäckning
166
 
167
  ```bash
168
- pytest --cov=src --cov-report=html
169
  ```
170
 
171
  Kör för att identifiera luckor i testtäckningen.
@@ -187,13 +189,14 @@ Kör för att identifiera luckor i testtäckningen.
187
  ## Framsteg
188
 
189
  - [x] Punkt 1: HDV-konsolidering ✅
190
- - [ ] Punkt 2: Ofullständiga features
191
  - [ ] Punkt 3: Felhantering
192
  - [ ] Punkt 4: Singleton-reduktion 📋 Roadmap
193
  - [ ] Punkt 5: Stora funktioner 📋 Roadmap
194
  - [x] Punkt 6: Circuit breakers ✅
195
  - [x] Punkt 7: Hårkodade sökvägar ✅
196
  - [x] Punkt 8: Import-stil ✅ (redan konsekvent)
 
197
 
198
  ---
199
 
 
26
  ---
27
 
28
  ### 2. Ofullständiga features
29
+ **Status:** ✅ Verified / Resolved
30
 
31
  **Problem:**
32
  - Flera TODOs i produktionskod som lämnats oimplementerade
 
44
  ```
45
 
46
  **Åtgärd:**
47
+ - `superposition_query`: Implemented as `_superposition_query` in `HAIMLLMIntegrator`.
48
+ - `orchestrate_orch_or`: Implemented in `HAIMEngine`.
49
+ - LLM Calls: Code now supports generic providers (OpenAI, Gemini via `google.generativeai`, etc) with safe fallbacks (`_mock_llm_response`) if not configured.
50
+ - `_concept_to_memory_id`: Implemented in `MultiAgentHAIM`.
51
 
52
  ---
53
 
 
167
  ## Förbättra testtäckning
168
 
169
  ```bash
170
+ pytest --cov=mnemocore --cov-report=html
171
  ```
172
 
173
  Kör för att identifiera luckor i testtäckningen.
 
189
  ## Framsteg
190
 
191
  - [x] Punkt 1: HDV-konsolidering ✅
192
+ - [x] Punkt 2: Ofullständiga features
193
  - [ ] Punkt 3: Felhantering
194
  - [ ] Punkt 4: Singleton-reduktion 📋 Roadmap
195
  - [ ] Punkt 5: Stora funktioner 📋 Roadmap
196
  - [x] Punkt 6: Circuit breakers ✅
197
  - [x] Punkt 7: Hårkodade sökvägar ✅
198
  - [x] Punkt 8: Import-stil ✅ (redan konsekvent)
199
+ - [x] Test-suite import fixad (src. -> mnemocore.) ✅
200
 
201
  ---
202
 
RELEASE_CHECKLIST.md CHANGED
@@ -1,59 +1,44 @@
1
- # MnemoCore Public Beta Release Checklist
2
 
3
- ## Status: 🟠 ORANGE → 🟢 GREEN
4
 
5
  ---
6
 
7
- ## ✅ Completed
8
 
9
  - [x] LICENSE file (MIT)
10
  - [x] .gitignore created
11
  - [x] data/memory.jsonl removed (no stored memories)
12
  - [x] No leaked API keys or credentials
13
- - [x] 82 unit tests passing
 
 
14
 
15
  ---
16
 
17
- ## 🔧 Code TODOs (Known Limitations)
18
 
19
- These are documented gaps that can ship as "Phase 4 roadmap" items:
20
 
21
- ### 1. `src/core/tier_manager.py:338`
22
- ```python
23
- pass # TODO: Implement full consolidation with Qdrant
24
- ```
25
- **Impact:** Warm→Cold tier consolidation limited
26
- **Workaround:** Hot→Warm works, Cold is filesystem-based
27
- **Fix:** Implement Qdrant batch scroll API for full archival
28
 
29
- ### 2. `src/core/engine.py:192`
30
- ```python
31
- # TODO: Phase 3.5 Qdrant search for WARM/COLD
32
- ```
33
- **Impact:** Query only searches HOT tier currently
34
- **Workaround:** Promote memories before querying
35
- **Fix:** Add async Qdrant similarity search in query()
36
-
37
- ### 3. `src/llm_integration.py:55-57, 128-129`
38
- ```python
39
- # TODO: Call Gemini 3 Pro via OpenClaw API
40
- reconstruction = "TODO: Call Gemini 3 Pro"
41
- ```
42
- **Impact:** LLM reconstruction not functional
43
- **Workaround:** Raw vector similarity works
44
- **Fix:** Implement LLM client or make it pluggable
45
 
46
- ### 4. `src/nightlab/engine.py:339`
47
- ```python
48
- # TODO: Notion API integration
49
- ```
50
- **Impact:** Session documentation not auto-pushed
51
- **Workaround:** Written to local markdown files
52
- **Fix:** Add optional Notion connector
 
 
53
 
54
  ---
55
 
56
- ## 📋 Pre-Release Actions
57
 
58
  ### Before git push:
59
 
@@ -62,7 +47,8 @@ reconstruction = "TODO: Call Gemini 3 Pro"
62
  rm -rf .pytest_cache __pycache__ */__pycache__ *.pyc
63
 
64
  # 2. Verify tests pass
65
- source .venv/bin/activate && python -m pytest tests/ -v
 
66
 
67
  # 3. Verify import works
68
  python -c "from mnemocore.core.engine import HAIMEngine; print('OK')"
@@ -72,54 +58,46 @@ grep -r "sk-" src/ --include="*.py"
72
  grep -r "api_key.*=" src/ --include="*.py" | grep -v "api_key=\"\""
73
 
74
  # 5. Initialize fresh data files
 
 
75
  touch data/memory.jsonl data/codebook.json data/concepts.json data/synapses.json
76
  ```
77
 
78
  ### Update README.md:
79
 
80
- - [ ] Add: "Beta Release - See RELEASE_CHECKLIST.md for known limitations"
81
- - [ ] Add: "Installation" section with `pip install -r requirements.txt`
82
- - [ ] Add: "Quick Start" example
83
- - [ ] Add: "Roadmap" section linking TODOs above
84
 
85
  ---
86
 
87
- ## 🚀 Release Command Sequence
88
 
89
  ```bash
90
- cd /home/dev-robin/Desktop/mnemocore
91
-
92
  # Verify clean state
93
  git status
94
 
95
- # Stage public files (exclude .venv)
96
- git add LICENSE .gitignore RELEASE_CHECKLIST.md
97
- git add src/ tests/ config.yaml requirements.txt pytest.ini
98
- git add README.md studycase.md docker-compose.yml
99
- git add data/.gitkeep # If exists, or create empty dirs
100
 
101
  # Commit
102
- git commit -m "Initial public beta release (MIT)
103
 
104
- Known limitations documented in RELEASE_CHECKLIST.md"
 
 
105
 
106
  # Tag
107
- git tag -a v0.1.0-beta -m "Public Beta Release"
108
 
109
- # Push (when ready)
110
  git push origin main --tags
111
  ```
112
 
113
  ---
114
 
115
- ## Post-Release
116
-
117
- - [ ] Create GitHub repository
118
- - [ ] Add repository topics: `vsa`, `holographic-memory`, `active-inference`, `vector-symbolic-architecture`
119
- - [ ] Enable GitHub Issues for community feedback
120
- - [ ] Publish whitepaper/blog post
121
-
122
- ---
123
-
124
- *Generated: 2026-02-15*
125
-
 
1
+ # MnemoCore Public Beta Release Checklist
2
 
3
+ ## Status: 🟢 GREEN
4
 
5
  ---
6
 
7
+ ## Completed
8
 
9
  - [x] LICENSE file (MIT)
10
  - [x] .gitignore created
11
  - [x] data/memory.jsonl removed (no stored memories)
12
  - [x] No leaked API keys or credentials
13
+ - [x] 377 unit tests passing (Coverage increased from 82)
14
+ - [x] Test suite import paths fixed (`src.` -> `mnemocore.`)
15
+ - [x] Critical TODOs addressed or verified as safe
16
 
17
  ---
18
 
19
+ ## 🔧 Resolved/Verified Items
20
 
21
+ The following items were previously listed as known limitations but have been verified as resolved or robustly handled:
22
 
23
+ 1. **Qdrant Consolidation:** `src/core/tier_manager.py` implements `consolidate_warm_to_cold` with full Qdrant batch scrolling.
24
+ 2. **Qdrant Search:** `src/core/engine.py` query pipeline correctly delegates to `TierManager.search` which queries Qdrant for WARM tier results.
25
+ 3. **LLM Integration:** `src/llm_integration.py` includes `_mock_llm_response` fallbacks when no provider is configured, ensuring stability even without API keys.
 
 
 
 
26
 
27
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ ## 📝 Remaining Roadmap Items (Non-Blocking)
30
+
31
+ ### 1. `src/llm_integration.py` - Advanced LLM Features
32
+ - **Status:** Functional with generic providers.
33
+ - **Task:** Implement specific "OpenClaw" or "Gemini 3 Pro" adapters if required in future. Current implementation supports generic OpenAI/Anthropic/Gemini/Ollama clients.
34
+
35
+ ### 2. Full Notion Integration
36
+ - **Status:** Not currently present in `src/mnemocore`.
37
+ - **Task:** Re-introduce `nightlab` or similar module if Notion support is needed in Phase 5.
38
 
39
  ---
40
 
41
+ ## 📋 Pre-Release Actions
42
 
43
  ### Before git push:
44
 
 
47
  rm -rf .pytest_cache __pycache__ */__pycache__ *.pyc
48
 
49
  # 2. Verify tests pass
50
+ # Note: Ensure you are in the environment where mnemocore is installed
51
+ python -m pytest
52
 
53
  # 3. Verify import works
54
  python -c "from mnemocore.core.engine import HAIMEngine; print('OK')"
 
58
  grep -r "api_key.*=" src/ --include="*.py" | grep -v "api_key=\"\""
59
 
60
  # 5. Initialize fresh data files
61
+ # Ensure data directory exists
62
+ mkdir -p data
63
  touch data/memory.jsonl data/codebook.json data/concepts.json data/synapses.json
64
  ```
65
 
66
  ### Update README.md:
67
 
68
+ - [x] Add: "Beta Release - See RELEASE_CHECKLIST.md for known limitations"
69
+ - [x] Add: "Installation" section with `pip install -r requirements.txt`
70
+ - [x] Add: "Quick Start" example
71
+ - [x] Add: "Roadmap" section linking TODOs above
72
 
73
  ---
74
 
75
+ ## 🚀 Release Command Sequence
76
 
77
  ```bash
 
 
78
  # Verify clean state
79
  git status
80
 
81
+ # Stage public files
82
+ git add LICENSE .gitignore RELEASE_CHECKLIST.md REFACTORING_TODO.md
83
+ git add src/ tests/ config.yaml requirements.txt pytest.ini pyproject.toml
84
+ git add README.md docker-compose.yml
85
+ git add data/.gitkeep # If exists
86
 
87
  # Commit
88
+ git commit -m "Release Candidate: All tests passing, critical TODOs resolved.
89
 
90
+ - Fixed test suite import paths (src -> mnemocore)
91
+ - Verified Qdrant consolidation and search implementation
92
+ - Confirmed LLM integration fallbacks"
93
 
94
  # Tag
95
+ git tag -a v0.5.0-beta -m "Public Beta Release"
96
 
97
+ # Push
98
  git push origin main --tags
99
  ```
100
 
101
  ---
102
 
103
+ *Updated: 2026-02-18*
 
 
 
 
 
 
 
 
 
 
config.yaml CHANGED
@@ -86,7 +86,7 @@ haim:
86
 
87
  # MCP (Model Context Protocol) bridge
88
  mcp:
89
- enabled: false
90
  transport: "stdio" # "stdio" recommended for local MCP clients
91
  host: "127.0.0.1"
92
  port: 8110
 
86
 
87
  # MCP (Model Context Protocol) bridge
88
  mcp:
89
+ enabled: true
90
  transport: "stdio" # "stdio" recommended for local MCP clients
91
  host: "127.0.0.1"
92
  port: 8110
data/mnemocore_hnsw.faiss ADDED
Binary file (6.95 kB). View file
 
data/mnemocore_hnsw_idmap.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id_map": ["test_node_1", "test_node_2", "test_node_0", "test_node_1", "test_node_2", "test_node_3", "test_node_4", "test_node_5", "test_node_6", "test_node_7", "test_node_8", "test_node_9", "f130a982-5936-4943-ac23-e1f4ad997d25", "19c168e1-c1c0-4074-bdc6-fa6c37bf5009", "b7ad4001-e20c-4f6d-8db7-eca98fbde4ea", "f22c991c-6336-4b11-867a-d141724253ff", "db802f41-0c60-4e62-8595-4864058924b2", "835b80bc-c2b1-4ecf-b0af-ec0442ec87bf", "d594c442-d4b5-4246-9c62-0fb605f235ca", "a467b3f5-d899-4b95-9e62-f7012171ec8d", "fdb78500-49fc-4cf3-a269-b023ba8fa11c", "21d4f6d9-72a6-4dd8-b83e-0d09ef57c680", "19f52711-1ae5-4c29-a0be-2618b162dace", "8442e4e6-cf0f-4f54-b35a-0ce6169fd2f3", "70010aa2-0d82-42bc-96f7-9b99374e24cd", "3cdb7cc9-44ce-4328-9d9a-44dc98d088eb", "1f12c17a-b4ef-4f21-b7f3-3df51196e7f6", "ac29dda1-fd96-4880-9141-01bc46ef93b1", "e0644808-6d24-470d-a667-867fe74d78e2", null, "836f116d-f47f-493b-9ac7-47db84e4a4f9", "fc37dc28-9e3a-484f-a65d-8bf46ee95dbb", "1d3fa01a-0710-453e-8738-4c5be348cd1b", "2776ca2a-429b-491d-9b8a-7061c9b14166", "a9cf8418-d188-41a8-b20d-f2431f790414", "eaf66116-a5ab-436a-a6c3-031d303cb6bd", "474ec49c-1f57-42ac-94e7-3d07d233780f", "a1a19beb-a5f8-4b31-9fe7-0db5b00415c6", "bad8ef94-7313-4bbf-a807-739ff8dcb5c2", "2f4bd760-18b2-4dc1-9dcb-3a6adb77ef23", "0c017bbc-e3ca-4ca0-be83-70b76c0b1be1", "3823dfcd-798d-4e59-a09e-113cc4ec6384", "2682da73-2827-4d68-83b5-1d88f92712ae", "280f0ca4-0930-4452-af29-392d2f99ba7b", "521f09c3-ed6c-4d0f-b04d-97362cb4a94d", "f338c239-dcbd-45ab-ab2b-4192084f5492", "2f011bbd-2a40-4666-a566-4117e8c5ed8e", "dae2e9c6-0c36-4536-b2ff-b59f5443c6fe", "d6028558-d803-454f-8a6f-f82463c44f18", "4c64fe2a-4194-494e-81a2-122911f6cd79", null, "n1", "n2", "n1"], "use_hnsw": false, "stale_count": 2}
data/mnemocore_hnsw_vectors.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:00400554e796c7717c4def78feb30b5ba5360efed6a37bd35f71738695ac6522
3
+ size 7040
data/subconscious_evolution.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
- "updated_at": "2026-02-18T18:55:55.471022+00:00",
3
- "cycle_count": 56,
4
  "insights_generated": 0,
5
  "current_cycle_interval": 1,
6
  "schedule": {
 
1
  {
2
+ "updated_at": "2026-02-21T21:14:22.859465+00:00",
3
+ "cycle_count": 168,
4
  "insights_generated": 0,
5
  "current_cycle_interval": 1,
6
  "schedule": {
docs/AGI_MEMORY_BLUEPRINT.md ADDED
@@ -0,0 +1,713 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore AGI Memory Blueprint
2
+ ### Toward a True Cognitive Memory Substrate for Agentic Systems
3
+
4
+ > This document defines the **Phase 5 “AGI Memory” architecture** for MnemoCore – transforming it from a high‑end hyperdimensional memory engine into a **general cognitive substrate** for autonomous AI agents.
5
+
6
+ ---
7
+
8
+ ## 0. Goals & Non‑Goals
9
+
10
+ ### 0.1 Core Goals
11
+
12
+ - Provide a **plug‑and‑play cognitive memory system** that any agent framework can mount as its “mind”:
13
+ - Solves **context window** limits by offloading long‑term structure and recall.
14
+ - Solves **memory management** by autonomous consolidation, forgetting, and self‑repair.
15
+ - Provides **new thoughts, associations and suggestions** rather than only retrieval.
16
+ - Implement an explicit, formal model of:
17
+ - **Working / Short‑Term Memory (WM/STM)**
18
+ - **Episodic Memory**
19
+ - **Semantic Memory**
20
+ - **Procedural / Skill Memory**
21
+ - **Meta‑Memory & Self‑Model**
22
+ - Maintain:
23
+ - `pip install mnemocore` **zero‑infra dev mode** (SQLite / in‑process vector store).
24
+ - Full infra path (Redis, Qdrant, k8s, MCP, OpenClaw live memory integration).[cite:436][cite:437]
25
+ - Provide **clean public APIs** (Python + HTTP + MCP) that:
26
+ - Give agents a minimal but powerful surface: `observe / recall / reflect / propose_change`.
27
+ - Are stable enough to build higher‑level frameworks on (LangGraph, AutoGen, OpenAI Agents, OpenClaw, custom stacks).
28
+
29
+ ### 0.2 Non‑Goals
30
+
31
+ - MnemoCore is **not**:
32
+ - En LLM eller policy‑generator.
33
+ - En komplett agentram – det är **minnet + kognitiva processer**.
34
+ - MnemoCore ska **inte** hårdkoda specifika LLM‑providers.
35
+ - LLM används via abstraherad integration (`SubconsciousAI`, `LLMIntegration`) så att byte av motor är trivialt.
36
+
37
+ ---
38
+
39
+ ## 1. Cognitive Architecture Overview
40
+
41
+ ### 1.1 High‑Level Mental Model
42
+
43
+ Systemet ska exponera en internt konsekvent kognitiv modell:
44
+
45
+ - **Working Memory (WM)**
46
+ - Korttidsbuffert per agent / samtal / uppgift.
47
+ - Håller aktuella mål, senaste steg, delresultat.
48
+ - Living in RAM, med explicit API.
49
+
50
+ - **Episodic Memory (EM)**
51
+ - Sekvens av *episodes*: “agent X gjorde Y i kontext Z och fick utfallet U”.
52
+ - Tidsstämplad, med länkar mellan episoder (kedjor).
53
+ - Riktad mot “vad hände när, i vilken ordning”.
54
+
55
+ - **Semantic Memory (SM)**
56
+ - Abstraherade, konsoliderade representationer (concepts, prototypes).
57
+ - Sammanfattningar av hundratals episoder → en “semantic anchor”.
58
+ - Bra för svar på “vad vet jag generellt om X?”.
59
+
60
+ - **Procedural Memory (PM)**
61
+ - Skills, planer, recept: “för att lösa typ‑X problem, gör följande steg …”.
62
+ - Kan hålla både mänsklig läsbar text och exekverbar kod (snippets, tools).
63
+
64
+ - **Meta‑Memory (MM)**
65
+ - Självmodell för MnemoCore själv: prestanda, reliability, konfiguration, kända svagheter.
66
+ - Driver **självförbättringsloopen**.
67
+
68
+ Alla dessa lever ovanpå din befintliga HDV/VSA‑kärna, tier manager, synapse index, subconscious loop osv.[cite:436][cite:437]
69
+
70
+ ### 1.2 New Core Services
71
+
72
+ Föreslagna nya Python‑moduler (under `src/mnemocore/core`):
73
+
74
+ - `memory_model.py`
75
+ - Typed dataklasser för WM/EM/SM/PM/MM entities.
76
+ - `working_memory.py`
77
+ - WM implementation per agent/task med snabb caching.
78
+ - `episodic_store.py`
79
+ - Episodisk tidsserie, sekvens‑API.
80
+ - `semantic_store.py`
81
+ - Wrapper ovanpå befintlig vektorstore (Qdrant/HDV/HNSW) + consolidation hooks.
82
+ - `procedural_store.py`
83
+ - Lagret för skills, scripts, tool definitions.
84
+ - `meta_memory.py`
85
+ - Självmodell, logik för self‑improvement proposals.
86
+ - `pulse.py`
87
+ - “Heartbeat”‑loop: driver subtle thoughts, consolidation ticks, gap detection, self‑reflection.
88
+ - `agent_profile.py`
89
+ - Persistent profil per agent: preferenser, styrkor/svagheter, quirks.
90
+
91
+ ---
92
+
93
+ ## 2. Data Model
94
+
95
+ ### 2.1 Working Memory (WM)
96
+
97
+ ```python
98
+ # src/mnemocore/core/memory_model.py
99
+
100
+ @dataclass
101
+ class WorkingMemoryItem:
102
+ id: str
103
+ agent_id: str
104
+ created_at: datetime
105
+ ttl_seconds: int
106
+ content: str
107
+ kind: Literal["thought", "observation", "goal", "plan_step", "meta"]
108
+ importance: float #
109
+ tags: list[str]
110
+ hdv: BinaryHDV | None
111
+
112
+ @dataclass
113
+ class WorkingMemoryState:
114
+ agent_id: str
115
+ items: list[WorkingMemoryItem]
116
+ max_items: int
117
+ ```
118
+
119
+ **Invariantes:**
120
+
121
+ - WM är *liten* (t.ex. 32–128 items per agent).
122
+ - WM ligger primärt i RAM; kan serialiseras till Redis/SQLite för persistens.
123
+ - Access är O(1)/O(log n); LRU + importance‑vägning vid evicering.
124
+
125
+ ### 2.2 Episodic Memory (EM)
126
+
127
+ ```python
128
+ @dataclass
129
+ class EpisodeEvent:
130
+ timestamp: datetime
131
+ kind: Literal["observation", "action", "thought", "reward", "error"]
132
+ content: str
133
+ metadata: dict[str, Any]
134
+ hdv: BinaryHDV
135
+
136
+ @dataclass
137
+ class Episode:
138
+ id: str
139
+ agent_id: str
140
+ started_at: datetime
141
+ ended_at: datetime | None
142
+ goal: str | None
143
+ context: str | None # project / environment
144
+ events: list[EpisodeEvent]
145
+ outcome: Literal["success", "failure", "partial", "unknown"]
146
+ reward: float | None
147
+ links_prev: list[str] # previous episode IDs
148
+ links_next: list[str] # next episode IDs
149
+ ltp_strength: float
150
+ reliability: float
151
+ ```
152
+
153
+ ### 2.3 Semantic Memory (SM)
154
+
155
+ ```python
156
+ @dataclass
157
+ class SemanticConcept:
158
+ id: str
159
+ label: str # "fastapi-request-validation"
160
+ description: str
161
+ tags: list[str]
162
+ prototype_hdv: BinaryHDV
163
+ support_episode_ids: list[str] # episodes som gav upphov
164
+ reliability: float
165
+ last_updated_at: datetime
166
+ metadata: dict[str, Any]
167
+ ```
168
+
169
+ Kopplas direkt mot consolidation/semantic_consolidation + codebook/immunology.[cite:436][cite:437]
170
+
171
+ ### 2.4 Procedural Memory (PM)
172
+
173
+ ```python
174
+ @dataclass
175
+ class ProcedureStep:
176
+ order: int
177
+ instruction: str
178
+ code_snippet: str | None
179
+ tool_call: dict[str, Any] | None
180
+
181
+ @dataclass
182
+ class Procedure:
183
+ id: str
184
+ name: str
185
+ description: str
186
+ created_by_agent: str | None
187
+ created_at: datetime
188
+ updated_at: datetime
189
+ steps: list[ProcedureStep]
190
+ trigger_pattern: str # "if user asks about X and Y"
191
+ success_count: int
192
+ failure_count: int
193
+ reliability: float
194
+ tags: list[str]
195
+ ```
196
+
197
+ Procedurer kan genereras av LLM (SubconsciousAI), testas i episodiskt minne, och sedan promotas/demotas med reliability‑loop.
198
+
199
+ ### 2.5 Meta‑Memory (MM)
200
+
201
+ ```python
202
+ @dataclass
203
+ class SelfMetric:
204
+ name: str # "hot_tier_hit_rate", "avg_query_latency_ms"
205
+ value: float
206
+ window: str # "5m", "1h", "24h"
207
+ updated_at: datetime
208
+
209
+ @dataclass
210
+ class SelfImprovementProposal:
211
+ id: str
212
+ created_at: datetime
213
+ author: Literal["system", "agent", "human"]
214
+ title: str
215
+ description: str
216
+ rationale: str
217
+ expected_effect: str
218
+ status: Literal["pending", "accepted", "rejected", "implemented"]
219
+ metadata: dict[str, Any]
220
+ ```
221
+
222
+ MM lagras delvis i vanlig storage (SM/PM) men har egen API‑yta.
223
+
224
+ ---
225
+
226
+ ## 3. Service Layer Design
227
+
228
+ ### 3.1 Working Memory Service
229
+
230
+ **Fil:** `src/mnemocore/core/working_memory.py`
231
+
232
+ Ansvar:
233
+
234
+ - Hålla en per‑agent WM‑state.
235
+ - Explicita operationer:
236
+ - `push_item(agent_id, item: WorkingMemoryItem)`
237
+ - `get_state(agent_id) -> WorkingMemoryState`
238
+ - `clear(agent_id)`
239
+ - `prune(agent_id)` – enligt importance + LRU.
240
+ - Integrera med engine/query:
241
+ - Vid varje query: WM får en snapshot av top‑K resultat som “context items”.
242
+ - Vid svar: agent kan markera vilka items som var relevanta.
243
+
244
+ ### 3.2 Episodic Store Service
245
+
246
+ **Fil:** `src/mnemocore/core/episodic_store.py`
247
+
248
+ Ansvar:
249
+
250
+ - Skapa och uppdatera Episodes:
251
+ - `start_episode(agent_id, goal, context) -> episode_id`
252
+ - `append_event(episode_id, kind, content, metadata)`
253
+ - `end_episode(episode_id, outcome, reward)`
254
+ - Query:
255
+ - `get_episode(id)`
256
+ - `get_recent(agent_id, limit, context)`
257
+ - `find_similar_episodes(hdv, top_k)`
258
+ - Koppling till befintlig HDV + tier manager:
259
+ - Varje Episode får en “episode_hdv” (bundle över event‑HDVs).
260
+ - LTP + reliabilitet följer samma formel som övrig LTP.
261
+
262
+ ### 3.3 Semantic Store Service
263
+
264
+ **Fil:** `src/mnemocore/core/semantic_store.py`
265
+
266
+ Ansvar:
267
+
268
+ - Hålla SemanticConcepts + codebook.
269
+ - API:
270
+ - `upsert_concept(concept: SemanticConcept)`
271
+ - `find_nearby_concepts(hdv, top_k)`
272
+ - `get_concept(id)`
273
+ - Hookar mot:
274
+ - `semantic_consolidation.py` → abstraktioner / anchors.
275
+ - `immunology.py` → attractor cleanup.
276
+ - `recursive_synthesizer.py` → djup konceptsyntes.
277
+
278
+ ### 3.4 Procedural Store Service
279
+
280
+ **Fil:** `src/mnemocore/core/procedural_store.py`
281
+
282
+ Ansvar:
283
+
284
+ - Lagra och hämta Procedures.
285
+ - API:
286
+ - `store_procedure(proc: Procedure)`
287
+ - `get_procedure(id)`
288
+ - `find_applicable_procedures(query, agent_id)`
289
+ - `record_procedure_outcome(id, success: bool)`
290
+ - Integrera med:
291
+ - SubconsciousAI → generera nya procedurer från pattern i EM/SM.
292
+ - Reliability‑loopen → promota “verified” skills.
293
+
294
+ ### 3.5 Meta Memory Service
295
+
296
+ **Fil:** `src/mnemocore/core/meta_memory.py`
297
+
298
+ Ansvar:
299
+
300
+ - Hålla SelfMetrics + SelfImprovementProposals.
301
+ - API:
302
+ - `record_metric(metric: SelfMetric)`
303
+ - `list_metrics(filter...)`
304
+ - `create_proposal(...)`
305
+ - `update_proposal_status(id, status)`
306
+ - Integrera med:
307
+ - Pulse → skanna metrics och föreslå ändringar.
308
+ - LLM → generera förslagstexter (“self‑reflection reports”).
309
+
310
+ ---
311
+
312
+ ## 4. Pulse & Subtle Thoughts
313
+
314
+ ### 4.1 Pulse Definition
315
+
316
+ **Fil:** `src/mnemocore/core/pulse.py`
317
+
318
+ “Pulsen” är en central loop (async task, cron, eller k8s CronJob) som:
319
+
320
+ - Kör med konfigurerbart intervall (t.ex. var 10:e sekund–var 5:e minut).
321
+ - Har ett definierat set “ticks”:
322
+
323
+ ```python
324
+ class PulseTick(Enum):
325
+ WM_MAINTENANCE = "wm_maintenance"
326
+ EPISODIC_CHAINING = "episodic_chaining"
327
+ SEMANTIC_REFRESH = "semantic_refresh"
328
+ GAP_DETECTION = "gap_detection"
329
+ INSIGHT_GENERATION = "insight_generation"
330
+ PROCEDURE_REFINEMENT = "procedure_refinement"
331
+ META_SELF_REFLECTION = "meta_self_reflection"
332
+ ```
333
+
334
+ Pulse orchestrerar:
335
+
336
+ - **WM_MAINTENANCE**
337
+ - Prune WM per agent.
338
+ - Lyfta nyligen viktiga items (“keep in focus”).
339
+
340
+ - **EPISODIC_CHAINING**
341
+ - Skapa/länka episodiska sekvenser (prev/next).
342
+ - “Temporala narrativ”.
343
+
344
+ - **SEMANTIC_REFRESH**
345
+ - Uppdatera semantic concepts baserat på nya episoder.
346
+ - Trigga immunology cleanup för drift.
347
+
348
+ - **GAP_DETECTION**
349
+ - Kör `GapDetector` över EM/SM sista N minuter/timmar.
350
+ - Producera strukturerade knowledge gaps.
351
+
352
+ - **INSIGHT_GENERATION**
353
+ - Kör SubconsciousAI/LLM över utvalda kluster.
354
+ - Skapar nya SemanticConcepts, Procedures, eller MetaProposals.
355
+
356
+ - **PROCEDURE_REFINEMENT**
357
+ - Uppdatera reliability över PM.
358
+ - Flagga outdated/farliga procedures.
359
+
360
+ - **META_SELF_REFLECTION**
361
+ - Sammanfattar senaste metriker, gap, failures → SelfImprovementProposals.
362
+
363
+ ### 4.2 Pulse Implementation Sketch
364
+
365
+ ```python
366
+ # src/mnemocore/core/pulse.py
367
+
368
+ class Pulse:
369
+ def __init__(self, container, config):
370
+ self.container = container
371
+ self.config = config
372
+ self._running = False
373
+
374
+ async def start(self):
375
+ self._running = True
376
+ while self._running:
377
+ start = datetime.utcnow()
378
+ await self.tick()
379
+ elapsed = (datetime.utcnow() - start).total_seconds()
380
+ await asyncio.sleep(max(0, self.config.pulse_interval_seconds - elapsed))
381
+
382
+ async def tick(self):
383
+ await self._wm_maintenance()
384
+ await self._episodic_chaining()
385
+ await self._semantic_refresh()
386
+ await self._gap_detection()
387
+ await self._insight_generation()
388
+ await self._procedure_refinement()
389
+ await self._meta_self_reflection()
390
+ ```
391
+
392
+ Konfiguration i `config.yaml`:
393
+
394
+ ```yaml
395
+ haim:
396
+ pulse:
397
+ enabled: true
398
+ interval_seconds: 30
399
+ max_agents_per_tick: 50
400
+ max_episodes_per_tick: 200
401
+ ```
402
+
403
+ ---
404
+
405
+ ## 5. Agent‑Facing APIs (Python & HTTP & MCP)
406
+
407
+ ### 5.1 High‑Level Python API
408
+
409
+ **Fil:** `src/mnemocore/agent_interface.py`
410
+
411
+ Syfte: ge agent‑kod ett ENKELT API:
412
+
413
+ ```python
414
+ class CognitiveMemoryClient:
415
+ def __init__(self, engine: HAIMEngine, wm, episodic, semantic, procedural, meta):
416
+ ...
417
+
418
+ # --- Observation & WM ---
419
+ def observe(self, agent_id: str, content: str, **meta) -> str: ...
420
+ def get_working_context(self, agent_id: str, limit: int = 16) -> list[WorkingMemoryItem]: ...
421
+
422
+ # --- Episodic ---
423
+ def start_episode(self, agent_id: str, goal: str, context: str | None = None) -> str: ...
424
+ def append_event(self, episode_id: str, kind: str, content: str, **meta) -> None: ...
425
+ def end_episode(self, episode_id: str, outcome: str, reward: float | None = None) -> None: ...
426
+
427
+ # --- Semantic / Retrieval ---
428
+ def recall(self, agent_id: str, query: str, context: str | None = None,
429
+ top_k: int = 8, modes: tuple[str, ...] = ("episodic","semantic")) -> list[dict]: ...
430
+
431
+ # --- Procedural ---
432
+ def suggest_procedures(self, agent_id: str, query: str, top_k: int = 5) -> list[Procedure]: ...
433
+ def record_procedure_outcome(self, proc_id: str, success: bool) -> None: ...
434
+
435
+ # --- Meta / Self-awareness ---
436
+ def get_knowledge_gaps(self, agent_id: str, lookback_hours: int = 24) -> list[dict]: ...
437
+ def get_self_improvement_proposals(self) -> list[SelfImprovementProposal]: ...
438
+ ```
439
+
440
+ ### 5.2 HTTP Layer Additions
441
+
442
+ Utöver befintliga `/store`, `/query`, `/feedback`, osv.[cite:437]
443
+
444
+ Nya endpoints:
445
+
446
+ - `POST /wm/observe`
447
+ - `GET /wm/{agent_id}`
448
+ - `POST /episodes/start`
449
+ - `POST /episodes/{id}/event`
450
+ - `POST /episodes/{id}/end`
451
+ - `GET /episodes/{id}`
452
+ - `GET /agents/{agent_id}/episodes`
453
+ - `GET /agents/{agent_id}/context`
454
+ - `GET /agents/{agent_id}/knowledge-gaps`
455
+ - `GET /procedures/search`
456
+ - `POST /procedures/{id}/feedback`
457
+ - `GET /meta/proposals`
458
+ - `POST /meta/proposals`
459
+
460
+ ### 5.3 MCP Tools
461
+
462
+ Utöka `mnemocore.mcp.server` med nya verktyg:
463
+
464
+ - `store_observation`
465
+ - `recall_context`
466
+ - `start_episode`, `end_episode`
467
+ - `query_memory`
468
+ - `get_knowledge_gaps`
469
+ - `get_self_improvement_proposals`
470
+
471
+ Så att Claude/GPT‑agenter kan:
472
+
473
+ - “Titta in” i agentens egen historik.
474
+ - Få WM + relevanta episoder + semantic concepts innan svar.
475
+ - Få gaps och self‑reflection prompts.
476
+
477
+ ---
478
+
479
+ ## 6. Self‑Improvement Loop
480
+
481
+ ### 6.1 Loop Definition
482
+
483
+ Målet: MnemoCore ska **ständigt förbättra sig**:
484
+
485
+ 1. Samlar **metrics** (performance + quality).
486
+ 2. Upptäcker systematiska brister (höga felrates, gap‑clusters).
487
+ 3. Genererar SelfImprovementProposals via LLM.
488
+ 4. Låter människa eller meta‑agent granska & appliera.
489
+
490
+ ### 6.2 Pipeline
491
+
492
+ 1. **Metrics Collection**
493
+ - Utnyttja befintlig `metrics.py` + Prometheus.[cite:436][cite:437]
494
+ - Exempelmetriker:
495
+ - `query_hit_rate`, `retrieval_latency_ms`
496
+ - `feedback_success_rate`, `feedback_failure_rate`
497
+ - `hot_tier_size`, `tier_promotion_rate`
498
+ - `gap_detection_count`, `gap_fill_count`
499
+
500
+ 2. **Issue Detection (Rule‑Based)**
501
+ - Batchjobb (Pulse) kör enkla regler:
502
+ - Om `feedback_failure_rate > X` för en viss tag (t.ex. “fastapi”) → skapa “knowledge area weak” flagg.
503
+ - Om `hot_tier_hit_rate < threshold` → dålig context‑masking eller fel tuned thresholds.
504
+
505
+ 3. **Proposal Generation (LLM)**
506
+ - `SubconsciousAI` får inputs:
507
+ - Metrics, knowledge gaps, failure cases, config snapshot.
508
+ - Prompt genererar:
509
+ - `SelfImprovementProposal.title/description/rationale`.
510
+
511
+ 4. **Review & Execution**
512
+ - API / UI för att lista proposals.
513
+ - Människa/agent accepterar/rejectar.
514
+ - Vid accept:
515
+ - Kan trigga config ändringar (med patch PR).
516
+ - Kan skapa GitHub issues/PR mallar.
517
+
518
+ ### 6.3 API
519
+
520
+ - `GET /meta/proposals`
521
+ - `POST /meta/proposals/{id}/status`
522
+
523
+ ---
524
+
525
+ ## 7. Association & “Subtle Thoughts”
526
+
527
+ ### 7.1 Association Engine
528
+
529
+ Målet: Systemet ska **själv föreslå**:
530
+
531
+ - Analogier (“det här liknar när vi gjorde X i annat projekt”).
532
+ - Relaterade koncept (“du pratar om Y, men Z har varit viktigt tidigare”).
533
+ - Långsiktiga teman och lärdomar.
534
+
535
+ Bygg vidare på:
536
+
537
+ - `synapse_index.py` (hebbian connections).[cite:436]
538
+ - `ripple_context.py` (kaskader).[cite:436]
539
+ - `recursive_synthesizer.py` (konceptsyntes).[cite:436]
540
+
541
+ Nya pattern:
542
+
543
+ - Vid varje Pulse:
544
+ - Hämta senaste N episoder.
545
+ - Kör k‑NN i semantic concept space.
546
+ - Kör ripple over synapses.
547
+ - Generera en uppsättning **CandidateAssociations**:
548
+
549
+ ```python
550
+ @dataclass
551
+ class CandidateAssociation:
552
+ id: str
553
+ agent_id: str
554
+ created_at: datetime
555
+ source_episode_ids: list[str]
556
+ related_concept_ids: list[str]
557
+ suggestion_text: str
558
+ confidence: float
559
+ ```
560
+
561
+ Lagra i SM/EM så att agent/LLM kan hämta “subtle thoughts” innan svar:
562
+
563
+ - `GET /agents/{agent_id}/subtle-thoughts`
564
+
565
+ ---
566
+
567
+ ## 8. Storage Backends & Profiles
568
+
569
+ ### 8.1 Profiles
570
+
571
+ Behåll pip‑enkelheten via profiler:
572
+
573
+ - **Lite Profile** (default, no extra deps):
574
+ - WM: in‑process dict
575
+ - EM: SQLite
576
+ - SM: in‑process HDV + mmap
577
+ - PM/MM: SQLite/JSON
578
+ - **Standard Profile**:
579
+ - WARM: Redis
580
+ - COLD: filesystem
581
+ - **Scale Profile**:
582
+ - WARM: Redis
583
+ - COLD: Qdrant (eller annan vector DB)
584
+ - Optionellt: S3 archive
585
+
586
+ Konfigurationsexempel:
587
+
588
+ ```yaml
589
+ haim:
590
+ profile: "lite" # "lite" | "standard" | "scale"
591
+ ```
592
+
593
+ ---
594
+
595
+ ## 9. OpenClaw & External Agents
596
+
597
+ ### 9.1 Designprincip för integration
598
+
599
+ För OpenClaw / liknande orchestrators:
600
+
601
+ - En agent definieras genom:
602
+ - `agent_id`
603
+ - `capabilities` (tools etc.)
604
+ - MnemoCore ska behandla `agent_id` som primär nyckel för:
605
+ - WM
606
+ - Episoder
607
+ - Preferenser
608
+ - Procedurer som agenten själv skapat
609
+
610
+ ### 9.2 “Live Memory” Pattern
611
+
612
+ - När OpenClaw kör:
613
+ - Varje observation → `observe(agent_id, content, meta)`
614
+ - Varje tool call / action → episod event.
615
+ - Före varje beslut:
616
+ - Hämta:
617
+ - `WM`
618
+ - `recent episodes`
619
+ - `relevant semantic concepts`
620
+ - `subtle thoughts` / associations
621
+ - `knowledge gaps` (om agenten vill använda dessa som frågor).
622
+
623
+ ---
624
+
625
+ ## 10. Testing & Evaluation Plan
626
+
627
+ ### 10.1 Unit & Integration Tests
628
+
629
+ Nya testfiler:
630
+
631
+ - `tests/test_working_memory.py`
632
+ - `tests/test_episodic_store.py`
633
+ - `tests/test_semantic_store.py`
634
+ - `tests/test_procedural_store.py`
635
+ - `tests/test_meta_memory.py`
636
+ - `tests/test_pulse.py`
637
+ - `tests/test_agent_interface.py`
638
+
639
+ Fokus:
640
+
641
+ - Invariantes (max WM size, LTP thresholds, reliability‑update).
642
+ - Episodic chaining korrekt.
643
+ - Semantic consolidation integration med nya SM‑API:t.
644
+ - Pulse tick ordering & time budget.
645
+
646
+ ### 10.2 Behavioural Benchmarks
647
+
648
+ Skapa `benchmarks/AGI_MEMORY_SCENARIOS.md`:
649
+
650
+ - Multi‑session tasks där agent måste:
651
+ - Minnas user preferences över dagar.
652
+ - Lära sig av failed attempts (feedback).
653
+ - Använda analogier över domäner.
654
+
655
+ Mät:
656
+
657
+ - Context reuse rate.
658
+ - Time‑to‑solve vs “no memory” baseline.
659
+ - Antal genererade self‑improvement proposals som faktiskt förbättrar outcomes.
660
+
661
+ ---
662
+
663
+ ## 11. Implementation Roadmap
664
+
665
+ ### Phase 5.0 – Core Structure
666
+
667
+ 1. Introduce `memory_model.py`, `working_memory.py`, `episodic_store.py`, `semantic_store.py`, `procedural_store.py`, `meta_memory.py`, `pulse.py`.
668
+ 2. Wire everything in `container.py` (new providers).
669
+ 3. Add `CognitiveMemoryClient` + minimal tests.
670
+
671
+ ### Phase 5.1 – WM/EM/SM in Engine
672
+
673
+ 4. Integrate WM into engine query/store paths.
674
+ 5. Integrate EM creation in API (store/query/feedback).
675
+ 6. Adapt semantic_consolidation/immunology to new SM service.
676
+
677
+ ### Phase 5.2 – Procedural & Association
678
+
679
+ 7. Implement procedural store + reliability integration.
680
+ 8. Build association engine + subtle thoughts endpoints.
681
+
682
+ ### Phase 5.3 – Self‑Improvement
683
+
684
+ 9. Wire metrics → meta_memory → proposals via SubconsciousAI.
685
+ 10. Add endpoints & optional small UI for proposals.
686
+
687
+ ### Phase 5.4 – Hardening & Agents
688
+
689
+ 11. Harden profiles (lite/standard/scale).
690
+ 12. Build reference integrations (OpenClaw, LangGraph, AutoGen).
691
+
692
+ ---
693
+
694
+ ## 12. Developer Notes
695
+
696
+ - Håll **backwards compatibility** på API där det går:
697
+ - Nya endpoints → prefix `v2` om nödvändigt.
698
+ - Python API kan vara “ny high‑level layer” ovanpå befintlig `HAIMEngine`.
699
+ - All ny funktionalitet **feature‑flaggas i config**:
700
+ - `haim.pulse.enabled`
701
+ - `haim.episodic.enabled`
702
+ - `haim.procedural.enabled`
703
+ - etc.
704
+ - Strikt logging / metrics för allt nytt:
705
+ - `haim_pulse_tick_duration_seconds`
706
+ - `haim_wm_size`
707
+ - `haim_episode_count`
708
+ - `haim_procedure_success_rate`
709
+ - `haim_self_proposals_pending`
710
+
711
+ ---
712
+
713
+ *This blueprint is the contract between MnemoCore, its agents, and its contributors. The intention is to let autonomous AI agents, human developers, and MnemoCore itself co‑evolve toward a truly cognitive memory substrate – one that remembers, forgets, reflects, and grows.*
integrations/README.md ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore Integrations
2
+
3
+ Connect MnemoCore's persistent cognitive memory to your AI coding tools.
4
+
5
+ ## Supported tools
6
+
7
+ | Tool | Method | Notes |
8
+ |------|--------|-------|
9
+ | **Claude Code** | MCP server + hooks + CLAUDE.md | Best integration — native tool access |
10
+ | **Gemini CLI** | GEMINI.md + wrapper script | Context injected at session start |
11
+ | **Aider** | Wrapper script (`--system-prompt`) | Context injected at session start |
12
+ | **Any CLI tool** | Universal shell scripts | Pipe context into any tool |
13
+ | **Open-source agents** | REST API (`mnemo_bridge.py`) | Minimal Python dependency |
14
+
15
+ ---
16
+
17
+ ## Quick setup
18
+
19
+ ### Prerequisites
20
+
21
+ 1. MnemoCore running: `uvicorn mnemocore.api.main:app --port 8100`
22
+ 2. `HAIM_API_KEY` environment variable set
23
+ 3. Python 3.10+ with `requests` (`pip install requests`)
24
+
25
+ ### Linux / macOS
26
+
27
+ ```bash
28
+ cd integrations/
29
+ bash setup.sh --all
30
+ ```
31
+
32
+ ### Windows (PowerShell)
33
+
34
+ ```powershell
35
+ cd integrations\
36
+ .\setup.ps1 -All
37
+ ```
38
+
39
+ ### Manual: test the bridge first
40
+
41
+ ```bash
42
+ export MNEMOCORE_URL=http://localhost:8100
43
+ export HAIM_API_KEY=your-key-here
44
+
45
+ python integrations/mnemo_bridge.py health
46
+ python integrations/mnemo_bridge.py context --top-k 5
47
+ python integrations/mnemo_bridge.py store "Fixed import error in engine.py" --tags "bugfix,python"
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Claude Code (recommended)
53
+
54
+ The setup script does three things:
55
+
56
+ ### 1. MCP server — native tool access
57
+
58
+ Registers MnemoCore as an MCP server in `~/.claude/mcp.json`.
59
+ Claude Code gets four tools:
60
+ - `memory_query` — search memories
61
+ - `memory_store` — store a memory
62
+ - `memory_get` / `memory_delete` — manage individual memories
63
+
64
+ Claude will use these automatically when you instruct it to remember things,
65
+ or you can configure CLAUDE.md to trigger them on every session (see below).
66
+
67
+ **Verify:** Run `claude mcp list` — you should see `mnemocore` listed.
68
+
69
+ ### 2. Hooks — automatic background storage
70
+
71
+ Two hooks are installed in `~/.claude/settings.json`:
72
+
73
+ - **PreToolUse** (`pre_session_inject.py`): On the first tool call of a session,
74
+ queries MnemoCore and injects recent context into Claude's awareness.
75
+
76
+ - **PostToolUse** (`post_tool_store.py`): After every `Edit`/`Write` call,
77
+ stores a lightweight memory entry in the background (non-blocking).
78
+
79
+ Hooks never block Claude Code — they degrade silently if MnemoCore is offline.
80
+
81
+ ### 3. CLAUDE.md — behavioral instructions
82
+
83
+ The setup appends memory usage instructions to `CLAUDE.md`.
84
+ This tells Claude *when* to use memory tools proactively.
85
+
86
+ ---
87
+
88
+ ## Gemini CLI
89
+
90
+ ```bash
91
+ # Option A: Use wrapper (injects context automatically)
92
+ alias gemini='bash integrations/gemini_cli/gemini_wrap.sh'
93
+ gemini "Fix the async bug in engine.py"
94
+
95
+ # Option B: Manual context injection
96
+ CONTEXT=$(integrations/universal/context_inject.sh)
97
+ gemini --system-prompt "$CONTEXT" "Fix the async bug in engine.py"
98
+ ```
99
+
100
+ Also add instructions to your `GEMINI.md`:
101
+ ```bash
102
+ cat integrations/gemini_cli/GEMINI_memory_snippet.md >> GEMINI.md
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Aider
108
+
109
+ ```bash
110
+ # Option A: Use wrapper
111
+ alias aider='bash integrations/aider/aider_wrap.sh'
112
+ aider --model claude-3-5-sonnet-20241022 engine.py
113
+
114
+ # Option B: Manual
115
+ CONTEXT=$(integrations/universal/context_inject.sh "async engine")
116
+ aider --system-prompt "$CONTEXT" engine.py
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Universal / Open-source agents
122
+
123
+ Any tool that accepts a system prompt can use MnemoCore:
124
+
125
+ ```bash
126
+ # Get context as markdown
127
+ integrations/universal/context_inject.sh "query text" 6
128
+
129
+ # Use in any command
130
+ MY_CONTEXT=$(integrations/universal/context_inject.sh)
131
+ some-ai-cli --system "$MY_CONTEXT" "do the task"
132
+
133
+ # Store a memory after a session
134
+ integrations/universal/store_session.sh \
135
+ "Discovered that warm tier mmap files grow unbounded without consolidation" \
136
+ "discovery,warm-tier,storage" \
137
+ "mnemocore-project"
138
+ ```
139
+
140
+ ### REST API (Python / any language)
141
+
142
+ ```python
143
+ import os, requests
144
+
145
+ BASE = os.getenv("MNEMOCORE_URL", "http://localhost:8100")
146
+ KEY = os.getenv("HAIM_API_KEY", "")
147
+ HDR = {"X-API-Key": KEY}
148
+
149
+ # Query
150
+ r = requests.post(f"{BASE}/query", json={"query": "async bugs", "top_k": 5}, headers=HDR)
151
+ for m in r.json()["results"]:
152
+ print(m["score"], m["content"])
153
+
154
+ # Store
155
+ requests.post(f"{BASE}/store", json={
156
+ "content": "Found root cause of memory leak in consolidation worker",
157
+ "metadata": {"source": "my-agent", "tags": ["bugfix", "memory"]}
158
+ }, headers=HDR)
159
+ ```
160
+
161
+ ---
162
+
163
+ ## Environment variables
164
+
165
+ | Variable | Default | Description |
166
+ |----------|---------|-------------|
167
+ | `MNEMOCORE_URL` | `http://localhost:8100` | MnemoCore API base URL |
168
+ | `HAIM_API_KEY` | — | API key (same as MnemoCore's `HAIM_API_KEY`) |
169
+ | `MNEMOCORE_TIMEOUT` | `5` | Request timeout in seconds |
170
+ | `MNEMOCORE_CONTEXT_DIR` | `~/.claude/mnemo_context` | Where hook writes context files |
171
+
172
+ ---
173
+
174
+ ## Architecture
175
+
176
+ ```
177
+ ┌─────────────────────────────────────────────────────┐
178
+ │ AI Coding Tool │
179
+ │ (Claude Code / Gemini CLI / Aider / Custom) │
180
+ └──────────────┬──────────────────────┬───────────────┘
181
+ │ MCP tools │ System prompt
182
+ │ (Claude Code only) │ (all tools)
183
+ ▼ ▼
184
+ ┌──────────────────────┐ ┌───────────────────────────┐
185
+ │ mnemocore MCP server│ │ mnemo_bridge.py CLI │
186
+ │ (stdio transport) │ │ (lightweight wrapper) │
187
+ └──────────┬───────────┘ └─────────────┬─────────────┘
188
+ │ │
189
+ └────────────┬───────────────┘
190
+ │ HTTP REST
191
+
192
+ ┌────────────────────────┐
193
+ │ MnemoCore API │
194
+ │ localhost:8100 │
195
+ │ │
196
+ │ ┌──────────────────┐ │
197
+ │ │ HAIMEngine │ │
198
+ │ │ HOT/WARM/COLD │ │
199
+ │ │ HDV vectors │ │
200
+ │ └──────────────────┘ │
201
+ └────────────────────────┘
202
+ ```
203
+
204
+ ---
205
+
206
+ ## Troubleshooting
207
+
208
+ **MnemoCore offline:**
209
+ ```bash
210
+ python integrations/mnemo_bridge.py health
211
+ # → MnemoCore is OFFLINE
212
+ # Start it: uvicorn mnemocore.api.main:app --port 8100
213
+ ```
214
+
215
+ **API key error (401):**
216
+ ```bash
217
+ export HAIM_API_KEY="your-key-from-.env"
218
+ python integrations/mnemo_bridge.py health
219
+ ```
220
+
221
+ **Hook not triggering (Claude Code):**
222
+ ```bash
223
+ # Check settings.json
224
+ cat ~/.claude/settings.json | python -m json.tool | grep -A5 hooks
225
+ ```
226
+
227
+ **MCP server not found (Claude Code):**
228
+ ```bash
229
+ # Verify mcp.json
230
+ cat ~/.claude/mcp.json
231
+ # Check PYTHONPATH includes src/
232
+ cd /path/to/mnemocore && python -m mnemocore.mcp.server --help
233
+ ```
integrations/aider/aider_wrap.sh ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # aider_wrap.sh — MnemoCore context injector for Aider
3
+ # ======================================================
4
+ # Usage: ./aider_wrap.sh [any aider args...]
5
+ #
6
+ # Injects MnemoCore memory context into Aider's system prompt
7
+ # using the --system-prompt flag (available in Aider 0.40+).
8
+ #
9
+ # Environment variables:
10
+ # MNEMOCORE_URL MnemoCore REST URL (default: http://localhost:8100)
11
+ # HAIM_API_KEY API key for MnemoCore
12
+ # BRIDGE_PY Path to mnemo_bridge.py (auto-detected)
13
+ # AIDER_BIN Path to aider binary (default: aider)
14
+
15
+ set -euo pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ BRIDGE_PY="${BRIDGE_PY:-$(realpath "$SCRIPT_DIR/../mnemo_bridge.py")}"
19
+ AIDER_BIN="${AIDER_BIN:-aider}"
20
+
21
+ # ── Fetch context ──────────────────────────────────────────────────────────
22
+ CONTEXT=""
23
+ if python3 "$BRIDGE_PY" health &>/dev/null 2>&1; then
24
+ CONTEXT="$(python3 "$BRIDGE_PY" context --top-k 6 2>/dev/null || true)"
25
+ fi
26
+
27
+ # ── Run Aider with or without injected context ─────────────────────────────
28
+ if [[ -n "$CONTEXT" ]]; then
29
+ PROMPT_FILE="$(mktemp /tmp/mnemo_aider_XXXXXX.md)"
30
+ trap 'rm -f "$PROMPT_FILE"' EXIT
31
+
32
+ cat > "$PROMPT_FILE" <<'HEREDOC'
33
+ ## MnemoCore: Memory from previous sessions
34
+
35
+ Use the following context from persistent memory to inform your work.
36
+ Do not repeat known decisions. Reference this to avoid re-discovering bugs.
37
+
38
+ HEREDOC
39
+ echo "$CONTEXT" >> "$PROMPT_FILE"
40
+
41
+ exec "$AIDER_BIN" --system-prompt "$PROMPT_FILE" "$@"
42
+ else
43
+ exec "$AIDER_BIN" "$@"
44
+ fi
integrations/claude_code/CLAUDE_memory_snippet.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore — Persistent Cognitive Memory
2
+
3
+ You have access to a persistent memory system via MCP tools:
4
+ - `memory_query` — search for relevant memories before starting any task
5
+ - `memory_store` — save important decisions, findings, and bug fixes after completing work
6
+ - `memory_stats` / `memory_health` — check system status
7
+
8
+ ## When to use memory
9
+
10
+ **At session start:** Call `memory_query` with the user's first message to retrieve relevant past context.
11
+
12
+ **After completing a task:** Call `memory_store` to record:
13
+ - What was changed and why (key architectural decisions)
14
+ - Bug fixes and root causes
15
+ - Non-obvious patterns discovered in the codebase
16
+ - User preferences and project conventions
17
+
18
+ **When you find something unexpected:** Store it immediately with relevant tags.
19
+
20
+ ## Storing memories
21
+
22
+ Include useful metadata:
23
+ ```json
24
+ {
25
+ "content": "Fixed async race condition in tier_manager.py by adding asyncio.Lock around promotion logic",
26
+ "metadata": {
27
+ "source": "claude-code",
28
+ "tags": ["bugfix", "async", "tier_manager"],
29
+ "project": "mnemocore"
30
+ }
31
+ }
32
+ ```
33
+
34
+ ## Rules
35
+ - Do NOT store trivial information (e.g., "the user asked me to open a file")
36
+ - DO store non-obvious insights, decisions with reasoning, and recurring patterns
37
+ - Query memory BEFORE reading files when working on a known codebase
38
+ - Store memory AFTER completing non-trivial changes
integrations/claude_code/hooks/post_tool_store.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Claude Code PostToolUse hook — MnemoCore auto-store
4
+ ====================================================
5
+ Automatically stores the result of significant file edits into MnemoCore.
6
+
7
+ Configure in ~/.claude/settings.json:
8
+ {
9
+ "hooks": {
10
+ "PostToolUse": [
11
+ {
12
+ "matcher": "Edit|Write",
13
+ "hooks": [
14
+ {
15
+ "type": "command",
16
+ "command": "python /path/to/post_tool_store.py"
17
+ }
18
+ ]
19
+ }
20
+ ]
21
+ }
22
+ }
23
+
24
+ The hook receives a JSON blob on stdin and exits 0 to allow the tool call.
25
+ It stores a lightweight memory entry in the background (non-blocking via
26
+ subprocess) so it never delays Claude Code's response.
27
+ """
28
+
29
+ import json
30
+ import os
31
+ import subprocess
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ BRIDGE = Path(__file__).resolve().parents[2] / "mnemo_bridge.py"
36
+ MIN_CONTENT_LEN = 30 # Ignore trivially short edits
37
+
38
+
39
+ def main() -> int:
40
+ try:
41
+ raw = sys.stdin.read()
42
+ data = json.loads(raw) if raw.strip() else {}
43
+ except json.JSONDecodeError:
44
+ return 0 # Never block Claude Code
45
+
46
+ tool_name = data.get("tool_name", "")
47
+ tool_input = data.get("tool_input", {})
48
+ session_id = data.get("session_id", "")
49
+
50
+ # Only act on file-writing tools
51
+ if tool_name not in {"Edit", "Write", "MultiEdit"}:
52
+ return 0
53
+
54
+ file_path = tool_input.get("file_path", "")
55
+ new_string = tool_input.get("new_string") or tool_input.get("content", "")
56
+
57
+ if not new_string or len(new_string) < MIN_CONTENT_LEN:
58
+ return 0
59
+
60
+ # Build a concise memory entry — just the file + a short excerpt
61
+ excerpt = new_string[:200].replace("\n", " ").strip()
62
+ memory_text = f"Modified {file_path}: {excerpt}"
63
+
64
+ tags = "claude-code,edit"
65
+ if file_path.endswith(".py"):
66
+ tags += ",python"
67
+ elif file_path.endswith((".ts", ".js")):
68
+ tags += ",javascript"
69
+
70
+ ctx = session_id[:16] if session_id else None
71
+
72
+ cmd = [
73
+ sys.executable, str(BRIDGE),
74
+ "store", memory_text,
75
+ "--source", "claude-code-hook",
76
+ "--tags", tags,
77
+ ]
78
+ if ctx:
79
+ cmd += ["--ctx", ctx]
80
+
81
+ env = {**os.environ}
82
+
83
+ # Fire-and-forget: do not block Claude Code
84
+ subprocess.Popen(
85
+ cmd,
86
+ env=env,
87
+ stdout=subprocess.DEVNULL,
88
+ stderr=subprocess.DEVNULL,
89
+ start_new_session=True,
90
+ )
91
+
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ sys.exit(main())
integrations/claude_code/hooks/pre_session_inject.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Claude Code PreToolUse hook — MnemoCore context injection
4
+ ==========================================================
5
+ On the FIRST tool call of a session, queries MnemoCore for recent context
6
+ and writes it to a temporary file that is referenced from CLAUDE.md.
7
+
8
+ This gives Claude Code automatic memory of previous sessions WITHOUT
9
+ requiring any explicit user commands.
10
+
11
+ Configure in ~/.claude/settings.json:
12
+ {
13
+ "hooks": {
14
+ "PreToolUse": [
15
+ {
16
+ "matcher": ".*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "python /path/to/pre_session_inject.py"
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }
27
+
28
+ The hook must exit 0 (allow) or 2 (block with message).
29
+ It never blocks — silently degrades if MnemoCore is offline.
30
+ """
31
+
32
+ import json
33
+ import os
34
+ import subprocess
35
+ import sys
36
+ import tempfile
37
+ from pathlib import Path
38
+
39
+ BRIDGE = Path(__file__).resolve().parents[2] / "mnemo_bridge.py"
40
+ CONTEXT_DIR = Path(os.getenv("MNEMOCORE_CONTEXT_DIR", Path.home() / ".claude" / "mnemo_context"))
41
+ DONE_FILE = CONTEXT_DIR / ".session_injected"
42
+
43
+
44
+ def main() -> int:
45
+ try:
46
+ raw = sys.stdin.read()
47
+ data = json.loads(raw) if raw.strip() else {}
48
+ except json.JSONDecodeError:
49
+ return 0
50
+
51
+ session_id = data.get("session_id", "")
52
+
53
+ # Only inject once per session
54
+ done_marker = CONTEXT_DIR / f".injected_{session_id[:16]}"
55
+ if done_marker.exists():
56
+ return 0
57
+
58
+ CONTEXT_DIR.mkdir(parents=True, exist_ok=True)
59
+
60
+ # Query MnemoCore for context
61
+ try:
62
+ result = subprocess.run(
63
+ [sys.executable, str(BRIDGE), "context", "--top-k", "8"],
64
+ capture_output=True,
65
+ text=True,
66
+ timeout=5,
67
+ env={**os.environ},
68
+ )
69
+ context_md = result.stdout.strip()
70
+ except (subprocess.TimeoutExpired, FileNotFoundError):
71
+ context_md = ""
72
+
73
+ if context_md:
74
+ context_file = CONTEXT_DIR / "latest_context.md"
75
+ context_file.write_text(context_md, encoding="utf-8")
76
+
77
+ # Mark session as injected
78
+ done_marker.touch()
79
+
80
+ # Output context as additional system information if available
81
+ if context_md:
82
+ # Claude Code hooks can output JSON to inject content
83
+ output = {
84
+ "type": "system_reminder",
85
+ "content": context_md,
86
+ }
87
+ print(json.dumps(output))
88
+
89
+ return 0
90
+
91
+
92
+ if __name__ == "__main__":
93
+ sys.exit(main())
integrations/claude_code/hooks_config_fragment.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_comment": "Merge this fragment into ~/.claude/settings.json under the 'hooks' key.",
3
+ "_note": "Replace MNEMOCORE_INTEGRATIONS_PATH with the absolute path to integrations/claude_code/hooks/",
4
+ "hooks": {
5
+ "PreToolUse": [
6
+ {
7
+ "matcher": ".*",
8
+ "hooks": [
9
+ {
10
+ "type": "command",
11
+ "command": "python MNEMOCORE_INTEGRATIONS_PATH/pre_session_inject.py"
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "PostToolUse": [
17
+ {
18
+ "matcher": "Edit|Write|MultiEdit",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "python MNEMOCORE_INTEGRATIONS_PATH/post_tool_store.py"
23
+ }
24
+ ]
25
+ }
26
+ ]
27
+ }
28
+ }
integrations/claude_code/mcp_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "mcpServers": {
3
+ "mnemocore": {
4
+ "command": "python",
5
+ "args": ["-m", "mnemocore.mcp.server"],
6
+ "cwd": "${MNEMOCORE_DIR}",
7
+ "env": {
8
+ "HAIM_API_KEY": "${HAIM_API_KEY}",
9
+ "PYTHONPATH": "${MNEMOCORE_DIR}/src"
10
+ }
11
+ }
12
+ }
13
+ }
integrations/gemini_cli/GEMINI_memory_snippet.md ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore — Persistent Cognitive Memory
2
+
3
+ You have access to a persistent memory system via the MnemoCore REST API at `$MNEMOCORE_URL` (default: `http://localhost:8100`).
4
+
5
+ ## Querying memory
6
+
7
+ To recall relevant context, call the API at the start of a task:
8
+
9
+ ```bash
10
+ curl -s -X POST "$MNEMOCORE_URL/query" \
11
+ -H "X-API-Key: $HAIM_API_KEY" \
12
+ -H "Content-Type: application/json" \
13
+ -d '{"query": "DESCRIBE_TASK_HERE", "top_k": 5}'
14
+ ```
15
+
16
+ ## Storing memory
17
+
18
+ After completing significant work, store a memory:
19
+
20
+ ```bash
21
+ curl -s -X POST "$MNEMOCORE_URL/store" \
22
+ -H "X-API-Key: $HAIM_API_KEY" \
23
+ -H "Content-Type: application/json" \
24
+ -d '{
25
+ "content": "WHAT_WAS_DONE_AND_WHY",
26
+ "metadata": {"source": "gemini-cli", "tags": ["relevant", "tags"]}
27
+ }'
28
+ ```
29
+
30
+ ## Guidelines
31
+
32
+ - **Query before starting** any non-trivial task on a known codebase
33
+ - **Store after completing** important changes, bug fixes, or design decisions
34
+ - **Do NOT store** trivial or ephemeral information
35
+ - Include relevant tags: language, component, type (bugfix/feature/refactor)
integrations/gemini_cli/gemini_wrap.sh ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # gemini_wrap.sh — MnemoCore context injector for Gemini CLI
3
+ # =============================================================
4
+ # Usage: ./gemini_wrap.sh [any gemini CLI args...]
5
+ #
6
+ # Fetches recent MnemoCore context and prepends it to the system prompt
7
+ # via a temporary file, then delegates to the real `gemini` binary.
8
+ #
9
+ # Environment variables:
10
+ # MNEMOCORE_URL MnemoCore REST URL (default: http://localhost:8100)
11
+ # HAIM_API_KEY API key for MnemoCore
12
+ # BRIDGE_PY Path to mnemo_bridge.py (auto-detected)
13
+ # GEMINI_BIN Path to gemini binary (default: gemini)
14
+
15
+ set -euo pipefail
16
+
17
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
+ BRIDGE_PY="${BRIDGE_PY:-$(realpath "$SCRIPT_DIR/../mnemo_bridge.py")}"
19
+ GEMINI_BIN="${GEMINI_BIN:-gemini}"
20
+ MNEMOCORE_URL="${MNEMOCORE_URL:-http://localhost:8100}"
21
+
22
+ # ── Fetch context (silently degrade if offline) ────────────────────────────
23
+ CONTEXT=""
24
+ if python3 "$BRIDGE_PY" health &>/dev/null; then
25
+ CONTEXT="$(python3 "$BRIDGE_PY" context --top-k 6 2>/dev/null || true)"
26
+ fi
27
+
28
+ # ── Build the injected system prompt fragment ──────────────────────────────
29
+ if [[ -n "$CONTEXT" ]]; then
30
+ MEMORY_FILE="$(mktemp /tmp/mnemo_context_XXXXXX.md)"
31
+ trap 'rm -f "$MEMORY_FILE"' EXIT
32
+
33
+ cat > "$MEMORY_FILE" <<'HEREDOC'
34
+ ## Persistent Memory Context (from MnemoCore)
35
+
36
+ The following is relevant context from your memory of previous sessions.
37
+ Use it to avoid re-discovering known patterns, bugs, and decisions.
38
+
39
+ HEREDOC
40
+ echo "$CONTEXT" >> "$MEMORY_FILE"
41
+
42
+ # Gemini CLI supports --system-prompt-file or similar flags.
43
+ # Adjust this to match the actual Gemini CLI interface.
44
+ exec "$GEMINI_BIN" --system-prompt-file "$MEMORY_FILE" "$@"
45
+ else
46
+ exec "$GEMINI_BIN" "$@"
47
+ fi
integrations/mnemo_bridge.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MnemoCore Bridge — Universal CLI
4
+ =================================
5
+ Lightweight bridge between MnemoCore REST API and any AI CLI tool.
6
+ No heavy dependencies: only stdlib + requests.
7
+
8
+ Usage:
9
+ python mnemo_bridge.py context [--query TEXT] [--top-k 5] [--ctx CTX_ID]
10
+ python mnemo_bridge.py store TEXT [--source SOURCE] [--tags TAG1,TAG2] [--ctx CTX_ID]
11
+ python mnemo_bridge.py health
12
+
13
+ Environment variables:
14
+ MNEMOCORE_URL Base URL of MnemoCore API (default: http://localhost:8100)
15
+ MNEMOCORE_API_KEY API key (same as HAIM_API_KEY)
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+ from typing import Any, Dict, List, Optional
23
+
24
+ try:
25
+ import requests
26
+ except ImportError:
27
+ print("ERROR: 'requests' package required. Run: pip install requests", file=sys.stderr)
28
+ sys.exit(1)
29
+
30
+ # ── Config ────────────────────────────────────────────────────────────────────
31
+ BASE_URL = os.getenv("MNEMOCORE_URL", "http://localhost:8100").rstrip("/")
32
+ API_KEY = os.getenv("MNEMOCORE_API_KEY") or os.getenv("HAIM_API_KEY", "")
33
+ TIMEOUT = int(os.getenv("MNEMOCORE_TIMEOUT", "5"))
34
+
35
+ HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}
36
+
37
+
38
+ # ── API helpers ───────────────────────────────────────────────────────────────
39
+
40
+ def _get(path: str) -> Optional[Dict]:
41
+ try:
42
+ r = requests.get(f"{BASE_URL}{path}", headers=HEADERS, timeout=TIMEOUT)
43
+ r.raise_for_status()
44
+ return r.json()
45
+ except requests.ConnectionError:
46
+ return None
47
+ except requests.HTTPError as e:
48
+ print(f"HTTP error: {e}", file=sys.stderr)
49
+ return None
50
+
51
+
52
+ def _post(path: str, payload: Dict) -> Optional[Dict]:
53
+ try:
54
+ r = requests.post(f"{BASE_URL}{path}", headers=HEADERS,
55
+ json=payload, timeout=TIMEOUT)
56
+ r.raise_for_status()
57
+ return r.json()
58
+ except requests.ConnectionError:
59
+ return None
60
+ except requests.HTTPError as e:
61
+ print(f"HTTP error: {e}", file=sys.stderr)
62
+ return None
63
+
64
+
65
+ # ── Commands ──────────────────────────────────────────────────────────────────
66
+
67
+ def cmd_health() -> int:
68
+ data = _get("/health")
69
+ if data is None:
70
+ print("MnemoCore is OFFLINE (could not connect)", file=sys.stderr)
71
+ return 1
72
+ status = data.get("status", "unknown")
73
+ print(f"MnemoCore status: {status}")
74
+ return 0 if status == "ok" else 1
75
+
76
+
77
+ def cmd_store(text: str, source: str, tags: List[str], ctx: Optional[str]) -> int:
78
+ metadata: Dict[str, Any] = {"source": source}
79
+ if tags:
80
+ metadata["tags"] = tags
81
+
82
+ payload: Dict[str, Any] = {"content": text, "metadata": metadata}
83
+ if ctx:
84
+ payload["agent_id"] = ctx
85
+
86
+ data = _post("/store", payload)
87
+ if data is None:
88
+ print("Failed to store memory (MnemoCore offline or error)", file=sys.stderr)
89
+ return 1
90
+
91
+ memory_id = data.get("id") or data.get("memory_id", "?")
92
+ print(f"Stored: {memory_id}")
93
+ return 0
94
+
95
+
96
+ def cmd_context(query: Optional[str], top_k: int, ctx: Optional[str]) -> int:
97
+ """
98
+ Fetch relevant memories and print them as a markdown block
99
+ suitable for injection into any AI tool's system prompt.
100
+ """
101
+ payload: Dict[str, Any] = {
102
+ "query": query or "recent work context decisions bugs fixes",
103
+ "top_k": top_k,
104
+ }
105
+ if ctx:
106
+ payload["agent_id"] = ctx
107
+
108
+ data = _post("/query", payload)
109
+ if data is None:
110
+ # Silently return empty — don't break the calling tool's startup
111
+ return 0
112
+
113
+ results: List[Dict] = data.get("results", [])
114
+ if not results:
115
+ return 0
116
+
117
+ lines = [
118
+ "<!-- MnemoCore: Persistent Memory Context -->",
119
+ "## Relevant memory from previous sessions\n",
120
+ ]
121
+ for r in results:
122
+ content = r.get("content", "").strip()
123
+ score = r.get("score", 0.0)
124
+ meta = r.get("metadata", {})
125
+ source = meta.get("source", "unknown")
126
+ tags = meta.get("tags", [])
127
+ tag_str = f" [{', '.join(tags)}]" if tags else ""
128
+
129
+ lines.append(f"- **[{source}{tag_str}]** (relevance {score:.2f}): {content}")
130
+
131
+ lines.append("\n<!-- End MnemoCore Context -->")
132
+ print("\n".join(lines))
133
+ return 0
134
+
135
+
136
+ # ── Entry point ───────────────────────────────────────────────────────────────
137
+
138
+ def main() -> int:
139
+ parser = argparse.ArgumentParser(
140
+ description="MnemoCore universal CLI bridge",
141
+ formatter_class=argparse.RawDescriptionHelpFormatter,
142
+ )
143
+ sub = parser.add_subparsers(dest="cmd", required=True)
144
+
145
+ # health
146
+ sub.add_parser("health", help="Check MnemoCore connectivity")
147
+
148
+ # store
149
+ p_store = sub.add_parser("store", help="Store a memory")
150
+ p_store.add_argument("text", help="Memory content")
151
+ p_store.add_argument("--source", default="cli", help="Source label")
152
+ p_store.add_argument("--tags", default="", help="Comma-separated tags")
153
+ p_store.add_argument("--ctx", default=None, help="Context/project ID")
154
+
155
+ # context
156
+ p_ctx = sub.add_parser("context", help="Fetch context as markdown")
157
+ p_ctx.add_argument("--query", default=None, help="Semantic query string")
158
+ p_ctx.add_argument("--top-k", type=int, default=5, help="Number of results")
159
+ p_ctx.add_argument("--ctx", default=None, help="Context/project ID")
160
+
161
+ args = parser.parse_args()
162
+
163
+ if args.cmd == "health":
164
+ return cmd_health()
165
+
166
+ if args.cmd == "store":
167
+ tags = [t.strip() for t in args.tags.split(",") if t.strip()]
168
+ return cmd_store(args.text, args.source, tags, args.ctx)
169
+
170
+ if args.cmd == "context":
171
+ return cmd_context(args.query, args.top_k, args.ctx)
172
+
173
+ return 1
174
+
175
+
176
+ if __name__ == "__main__":
177
+ sys.exit(main())
integrations/setup.ps1 ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # MnemoCore Integration Setup — Windows PowerShell
2
+ # =================================================
3
+ # One-command setup for Claude Code (MCP) on Windows.
4
+ # For full hook/wrapper support, use WSL or Git Bash.
5
+ #
6
+ # Usage:
7
+ # .\setup.ps1
8
+ # .\setup.ps1 -All
9
+ # .\setup.ps1 -ClaudeCode
10
+
11
+ param(
12
+ [switch]$All,
13
+ [switch]$ClaudeCode,
14
+ [switch]$Gemini,
15
+ [switch]$Aider
16
+ )
17
+
18
+ $ErrorActionPreference = "Stop"
19
+
20
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
21
+ $MnemoDir = Split-Path -Parent $ScriptDir
22
+ $BridgePy = Join-Path $ScriptDir "mnemo_bridge.py"
23
+ $ClaudeHome = Join-Path $env:USERPROFILE ".claude"
24
+ $ClaudeMcp = Join-Path $ClaudeHome "mcp.json"
25
+ $ClaudeSettings = Join-Path $ClaudeHome "settings.json"
26
+ $HooksDir = Join-Path $ScriptDir "claude_code\hooks"
27
+
28
+ function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Cyan }
29
+ function Write-Success { Write-Host "[OK] $args" -ForegroundColor Green }
30
+ function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow }
31
+ function Write-Err { Write-Host "[ERROR] $args" -ForegroundColor Red }
32
+
33
+ # ── Prerequisite checks ────────────────────────────────────────────────────
34
+
35
+ Write-Info "Checking Python requests..."
36
+ $requestsCheck = python -c "import requests; print('ok')" 2>&1
37
+ if ($requestsCheck -ne "ok") {
38
+ Write-Warn "Installing requests..."
39
+ python -m pip install --quiet requests
40
+ }
41
+ Write-Success "Python requests available"
42
+
43
+ Write-Info "Checking MnemoCore connectivity..."
44
+ $healthCheck = python "$BridgePy" health 2>&1
45
+ if ($LASTEXITCODE -eq 0) {
46
+ Write-Success "MnemoCore is online"
47
+ } else {
48
+ Write-Warn "MnemoCore offline — start it first:"
49
+ Write-Warn " cd $MnemoDir"
50
+ Write-Warn " uvicorn mnemocore.api.main:app --port 8100"
51
+ }
52
+
53
+ # ── Claude Code MCP Setup ─────────────────────────────────────────────────
54
+
55
+ function Setup-ClaudeCode {
56
+ Write-Info "Setting up Claude Code integration..."
57
+
58
+ if (-not (Test-Path $ClaudeHome)) { New-Item -ItemType Directory -Path $ClaudeHome | Out-Null }
59
+ New-Item -ItemType Directory -Path (Join-Path $ClaudeHome "mnemo_context") -Force | Out-Null
60
+
61
+ # MCP config
62
+ $McpTemplate = Get-Content (Join-Path $ScriptDir "claude_code\mcp_config.json") -Raw
63
+ $McpTemplate = $McpTemplate `
64
+ -replace '\$\{MNEMOCORE_DIR\}', $MnemoDir.Replace('\', '/') `
65
+ -replace '\$\{HAIM_API_KEY\}', ($env:HAIM_API_KEY ?? '')
66
+
67
+ if (-not (Test-Path $ClaudeMcp)) {
68
+ '{"mcpServers":{}}' | Set-Content $ClaudeMcp
69
+ }
70
+
71
+ $Existing = Get-Content $ClaudeMcp -Raw | ConvertFrom-Json
72
+ $New = $McpTemplate | ConvertFrom-Json
73
+ if (-not $Existing.mcpServers) { $Existing | Add-Member -MemberType NoteProperty -Name mcpServers -Value @{} }
74
+ $New.mcpServers.PSObject.Properties | ForEach-Object {
75
+ $Existing.mcpServers | Add-Member -MemberType NoteProperty -Name $_.Name -Value $_.Value -Force
76
+ }
77
+ $Existing | ConvertTo-Json -Depth 10 | Set-Content $ClaudeMcp
78
+ Write-Success "MCP server registered in $ClaudeMcp"
79
+
80
+ # Hooks
81
+ if (-not (Test-Path $ClaudeSettings)) { '{}' | Set-Content $ClaudeSettings }
82
+ $Settings = Get-Content $ClaudeSettings -Raw | ConvertFrom-Json
83
+
84
+ if (-not $Settings.hooks) {
85
+ $Settings | Add-Member -MemberType NoteProperty -Name hooks -Value @{}
86
+ }
87
+ $hooksObj = $Settings.hooks
88
+
89
+ $preCmd = "python `"$($HooksDir.Replace('\','/'))/pre_session_inject.py`""
90
+ $postCmd = "python `"$($HooksDir.Replace('\','/'))/post_tool_store.py`""
91
+
92
+ if (-not $hooksObj.PreToolUse) {
93
+ $hooksObj | Add-Member -MemberType NoteProperty -Name PreToolUse -Value @()
94
+ }
95
+ if (-not $hooksObj.PostToolUse) {
96
+ $hooksObj | Add-Member -MemberType NoteProperty -Name PostToolUse -Value @()
97
+ }
98
+
99
+ $existingPre = $hooksObj.PreToolUse | ForEach-Object { $_.hooks[0].command }
100
+ if ($preCmd -notin $existingPre) {
101
+ $hooksObj.PreToolUse += @{matcher=".*"; hooks=@(@{type="command"; command=$preCmd})}
102
+ }
103
+ $existingPost = $hooksObj.PostToolUse | ForEach-Object { $_.hooks[0].command }
104
+ if ($postCmd -notin $existingPost) {
105
+ $hooksObj.PostToolUse += @{matcher="Edit|Write|MultiEdit"; hooks=@(@{type="command"; command=$postCmd})}
106
+ }
107
+
108
+ $Settings | ConvertTo-Json -Depth 10 | Set-Content $ClaudeSettings
109
+ Write-Success "Hooks installed in $ClaudeSettings"
110
+
111
+ # CLAUDE.md snippet
112
+ $ClaudeMd = Join-Path $MnemoDir "CLAUDE.md"
113
+ $Snippet = Get-Content (Join-Path $ScriptDir "claude_code\CLAUDE_memory_snippet.md") -Raw
114
+ $Marker = "# MnemoCore — Persistent Cognitive Memory"
115
+ if (Test-Path $ClaudeMd) {
116
+ $Current = Get-Content $ClaudeMd -Raw
117
+ if ($Current -notlike "*$Marker*") {
118
+ Add-Content $ClaudeMd "`n$Snippet"
119
+ Write-Success "Memory instructions appended to CLAUDE.md"
120
+ } else {
121
+ Write-Info "CLAUDE.md already contains MnemoCore instructions"
122
+ }
123
+ } else {
124
+ $Snippet | Set-Content $ClaudeMd
125
+ Write-Success "Created CLAUDE.md with memory instructions"
126
+ }
127
+
128
+ Write-Success "Claude Code integration complete"
129
+ }
130
+
131
+ # ── Main ───────────────────────────────────────────────────────────────────
132
+
133
+ Write-Host ""
134
+ Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Magenta
135
+ Write-Host "║ MnemoCore Integration Setup (Win) ║" -ForegroundColor Magenta
136
+ Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Magenta
137
+ Write-Host ""
138
+
139
+ if (-not ($All -or $ClaudeCode -or $Gemini -or $Aider)) {
140
+ Write-Host "Choose integrations:"
141
+ Write-Host " 1) Claude Code (MCP + hooks + CLAUDE.md) — recommended"
142
+ Write-Host " 4) All"
143
+ $choice = Read-Host "Enter choice"
144
+ switch ($choice) {
145
+ "1" { $ClaudeCode = $true }
146
+ "4" { $All = $true }
147
+ }
148
+ }
149
+
150
+ if ($All -or $ClaudeCode) { Setup-ClaudeCode }
151
+
152
+ Write-Host ""
153
+ Write-Host "╔══════════════════════════════════════════╗" -ForegroundColor Green
154
+ Write-Host "║ Setup complete! ║" -ForegroundColor Green
155
+ Write-Host "║ ║" -ForegroundColor Green
156
+ Write-Host "║ Test: python integrations/mnemo_bridge.py health" -ForegroundColor Green
157
+ Write-Host "╚══════════════════════════════════════════╝" -ForegroundColor Green
158
+ Write-Host ""
integrations/setup.sh ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # MnemoCore Integration Setup
3
+ # ============================
4
+ # One-command setup for Claude Code, Gemini CLI, Aider, and universal tools.
5
+ #
6
+ # Usage:
7
+ # ./setup.sh # Interactive, choose integrations
8
+ # ./setup.sh --all # Enable all integrations
9
+ # ./setup.sh --claude-code # Claude Code only
10
+ # ./setup.sh --gemini # Gemini CLI only
11
+ # ./setup.sh --aider # Aider only
12
+ #
13
+ # Prerequisites:
14
+ # - Python 3.10+ with 'requests' package
15
+ # - MnemoCore running (uvicorn mnemocore.api.main:app --port 8100)
16
+ # - HAIM_API_KEY environment variable set
17
+
18
+ set -euo pipefail
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ MNEMOCORE_DIR="$(realpath "$SCRIPT_DIR/..")"
22
+ BRIDGE_PY="$SCRIPT_DIR/mnemo_bridge.py"
23
+ HOOKS_DIR="$SCRIPT_DIR/claude_code/hooks"
24
+
25
+ CLAUDE_SETTINGS="$HOME/.claude/settings.json"
26
+ CLAUDE_MCP="$HOME/.claude/mcp.json"
27
+
28
+ RED='\033[0;31m'
29
+ GREEN='\033[0;32m'
30
+ YELLOW='\033[1;33m'
31
+ BLUE='\033[0;34m'
32
+ NC='\033[0m'
33
+
34
+ # ── Helpers ────────────────────────────────────────────────────────────────
35
+
36
+ info() { echo -e "${BLUE}[INFO]${NC} $*"; }
37
+ success() { echo -e "${GREEN}[OK]${NC} $*"; }
38
+ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
39
+ error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
40
+
41
+ check_python() {
42
+ if ! python3 -c "import requests" &>/dev/null; then
43
+ warn "Python 'requests' not installed. Installing..."
44
+ python3 -m pip install --quiet requests
45
+ success "requests installed"
46
+ fi
47
+ }
48
+
49
+ check_mnemocore() {
50
+ info "Checking MnemoCore connectivity..."
51
+ if python3 "$BRIDGE_PY" health &>/dev/null; then
52
+ success "MnemoCore is online"
53
+ return 0
54
+ else
55
+ warn "MnemoCore is not running. Start it first with:"
56
+ warn " cd $MNEMOCORE_DIR && uvicorn mnemocore.api.main:app --port 8100"
57
+ return 1
58
+ fi
59
+ }
60
+
61
+ merge_json() {
62
+ # Merge JSON object $2 into file $1 (creates if not exists)
63
+ local target="$1"
64
+ local fragment="$2"
65
+
66
+ if [[ ! -f "$target" ]]; then
67
+ echo '{}' > "$target"
68
+ fi
69
+
70
+ python3 - <<PYEOF
71
+ import json, sys
72
+ with open("$target") as f:
73
+ existing = json.load(f)
74
+ with open("$fragment") as f:
75
+ new = json.load(f)
76
+ # Deep merge (one level)
77
+ for k, v in new.items():
78
+ if k.startswith("_"):
79
+ continue
80
+ if k in existing and isinstance(existing[k], dict) and isinstance(v, dict):
81
+ existing[k].update(v)
82
+ else:
83
+ existing[k] = v
84
+ with open("$target", "w") as f:
85
+ json.dump(existing, f, indent=2)
86
+ print("Merged successfully")
87
+ PYEOF
88
+ }
89
+
90
+ # ── Integration: Claude Code ───────────────────────────────────────────────
91
+
92
+ setup_claude_code() {
93
+ info "Setting up Claude Code integration..."
94
+ mkdir -p "$HOME/.claude/mnemo_context"
95
+
96
+ # 1. MCP Server
97
+ info " Configuring MCP server..."
98
+ local mcp_tmp
99
+ mcp_tmp="$(mktemp /tmp/mnemo_mcp_XXXXXX.json)"
100
+ sed \
101
+ -e "s|\${MNEMOCORE_DIR}|$MNEMOCORE_DIR|g" \
102
+ -e "s|\${HAIM_API_KEY}|${HAIM_API_KEY:-}|g" \
103
+ "$SCRIPT_DIR/claude_code/mcp_config.json" > "$mcp_tmp"
104
+
105
+ if [[ ! -f "$CLAUDE_MCP" ]]; then
106
+ echo '{"mcpServers": {}}' > "$CLAUDE_MCP"
107
+ fi
108
+
109
+ python3 - "$CLAUDE_MCP" "$mcp_tmp" <<'PYEOF'
110
+ import json, sys
111
+ with open(sys.argv[1]) as f:
112
+ existing = json.load(f)
113
+ with open(sys.argv[2]) as f:
114
+ new = json.load(f)
115
+ existing.setdefault("mcpServers", {}).update(new.get("mcpServers", {}))
116
+ with open(sys.argv[1], "w") as f:
117
+ json.dump(existing, f, indent=2)
118
+ PYEOF
119
+ rm -f "$mcp_tmp"
120
+ success " MCP server registered in $CLAUDE_MCP"
121
+
122
+ # 2. Hooks
123
+ info " Installing hooks in $CLAUDE_SETTINGS..."
124
+ if [[ ! -f "$CLAUDE_SETTINGS" ]]; then
125
+ echo '{}' > "$CLAUDE_SETTINGS"
126
+ fi
127
+
128
+ python3 - "$CLAUDE_SETTINGS" "$HOOKS_DIR" <<PYEOF
129
+ import json, sys
130
+ settings_path = sys.argv[1]
131
+ hooks_dir = sys.argv[2]
132
+ with open(settings_path) as f:
133
+ settings = json.load(f)
134
+
135
+ hooks = settings.setdefault("hooks", {})
136
+ pre = hooks.setdefault("PreToolUse", [])
137
+ post = hooks.setdefault("PostToolUse", [])
138
+
139
+ pre_hook = {
140
+ "matcher": ".*",
141
+ "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/pre_session_inject.py"}]
142
+ }
143
+ post_hook = {
144
+ "matcher": "Edit|Write|MultiEdit",
145
+ "hooks": [{"type": "command", "command": f"python3 {hooks_dir}/post_tool_store.py"}]
146
+ }
147
+
148
+ # Only add if not already present
149
+ pre_cmds = [h["hooks"][0]["command"] for h in pre if h.get("hooks")]
150
+ post_cmds = [h["hooks"][0]["command"] for h in post if h.get("hooks")]
151
+
152
+ if pre_hook["hooks"][0]["command"] not in pre_cmds:
153
+ pre.append(pre_hook)
154
+ if post_hook["hooks"][0]["command"] not in post_cmds:
155
+ post.append(post_hook)
156
+
157
+ with open(settings_path, "w") as f:
158
+ json.dump(settings, f, indent=2)
159
+ print("Hooks installed")
160
+ PYEOF
161
+ success " Hooks installed in $CLAUDE_SETTINGS"
162
+
163
+ # 3. CLAUDE.md snippet — append if not already present
164
+ local clause_md="$MNEMOCORE_DIR/CLAUDE.md"
165
+ local snippet="$SCRIPT_DIR/claude_code/CLAUDE_memory_snippet.md"
166
+ local marker="# MnemoCore — Persistent Cognitive Memory"
167
+ if [[ -f "$clause_md" ]] && grep -qF "$marker" "$clause_md"; then
168
+ info " CLAUDE.md already contains MnemoCore memory instructions"
169
+ else
170
+ echo "" >> "$clause_md"
171
+ cat "$snippet" >> "$clause_md"
172
+ success " Memory instructions appended to $clause_md"
173
+ fi
174
+
175
+ success "Claude Code integration complete"
176
+ }
177
+
178
+ # ── Integration: Gemini CLI ────────────────────────────────────────────────
179
+
180
+ setup_gemini() {
181
+ info "Setting up Gemini CLI integration..."
182
+
183
+ # Make wrapper executable
184
+ chmod +x "$SCRIPT_DIR/gemini_cli/gemini_wrap.sh"
185
+
186
+ # Append to GEMINI.md if it exists
187
+ local gemini_md="$MNEMOCORE_DIR/GEMINI.md"
188
+ local snippet="$SCRIPT_DIR/gemini_cli/GEMINI_memory_snippet.md"
189
+ local marker="# MnemoCore — Persistent Cognitive Memory"
190
+ if [[ -f "$gemini_md" ]] && grep -qF "$marker" "$gemini_md"; then
191
+ info " GEMINI.md already contains MnemoCore instructions"
192
+ elif [[ -f "$gemini_md" ]]; then
193
+ echo "" >> "$gemini_md"
194
+ cat "$snippet" >> "$gemini_md"
195
+ success " Memory instructions appended to $gemini_md"
196
+ else
197
+ cp "$snippet" "$gemini_md"
198
+ success " Created $gemini_md with memory instructions"
199
+ fi
200
+
201
+ success "Gemini CLI integration complete"
202
+ info " Use: $SCRIPT_DIR/gemini_cli/gemini_wrap.sh [args] instead of 'gemini'"
203
+ info " Or alias: alias gemini='$SCRIPT_DIR/gemini_cli/gemini_wrap.sh'"
204
+ }
205
+
206
+ # ── Integration: Aider ─────────────────────────────────────────────────────
207
+
208
+ setup_aider() {
209
+ info "Setting up Aider integration..."
210
+ chmod +x "$SCRIPT_DIR/aider/aider_wrap.sh"
211
+
212
+ # Write .env fragment for aider
213
+ local aider_env="$MNEMOCORE_DIR/.aider.env"
214
+ cat > "$aider_env" <<EOF
215
+ # MnemoCore environment for Aider
216
+ export MNEMOCORE_URL="${MNEMOCORE_URL:-http://localhost:8100}"
217
+ export HAIM_API_KEY="${HAIM_API_KEY:-}"
218
+ export BRIDGE_PY="$BRIDGE_PY"
219
+ EOF
220
+ success "Aider integration complete"
221
+ info " Use: $SCRIPT_DIR/aider/aider_wrap.sh [args] instead of 'aider'"
222
+ info " Or alias: alias aider='$SCRIPT_DIR/aider/aider_wrap.sh'"
223
+ }
224
+
225
+ # ── Integration: Universal scripts ─────────────────────────────────────────
226
+
227
+ setup_universal() {
228
+ chmod +x "$SCRIPT_DIR/universal/context_inject.sh"
229
+ chmod +x "$SCRIPT_DIR/universal/store_session.sh"
230
+ success "Universal scripts ready"
231
+ info " Context: $SCRIPT_DIR/universal/context_inject.sh [query] [top-k]"
232
+ info " Store: $SCRIPT_DIR/universal/store_session.sh [text] [tags] [ctx]"
233
+ }
234
+
235
+ # ── Main ───────────────────────────────────────────────────────────────────
236
+
237
+ echo ""
238
+ echo "╔══════════════════════════════════════════════════════╗"
239
+ echo "║ MnemoCore Integration Setup ║"
240
+ echo "╚══════════════════════════════════════════════════════╝"
241
+ echo ""
242
+
243
+ # Check prerequisites
244
+ check_python
245
+ check_mnemocore || true # Non-fatal — offline check is a warning
246
+
247
+ DO_ALL=false
248
+ DO_CLAUDE=false
249
+ DO_GEMINI=false
250
+ DO_AIDER=false
251
+
252
+ for arg in "$@"; do
253
+ case "$arg" in
254
+ --all) DO_ALL=true ;;
255
+ --claude-code) DO_CLAUDE=true ;;
256
+ --gemini) DO_GEMINI=true ;;
257
+ --aider) DO_AIDER=true ;;
258
+ esac
259
+ done
260
+
261
+ if ! $DO_ALL && ! $DO_CLAUDE && ! $DO_GEMINI && ! $DO_AIDER; then
262
+ echo "Which integrations do you want to enable?"
263
+ echo " 1) Claude Code (MCP + hooks + CLAUDE.md)"
264
+ echo " 2) Gemini CLI (GEMINI.md + wrapper)"
265
+ echo " 3) Aider (wrapper script)"
266
+ echo " 4) All of the above"
267
+ echo ""
268
+ read -rp "Enter choice(s) [e.g. 1 3 or 4]: " CHOICES
269
+
270
+ for c in $CHOICES; do
271
+ case "$c" in
272
+ 1) DO_CLAUDE=true ;;
273
+ 2) DO_GEMINI=true ;;
274
+ 3) DO_AIDER=true ;;
275
+ 4) DO_ALL=true ;;
276
+ esac
277
+ done
278
+ fi
279
+
280
+ if $DO_ALL; then
281
+ DO_CLAUDE=true; DO_GEMINI=true; DO_AIDER=true
282
+ fi
283
+
284
+ echo ""
285
+ setup_universal
286
+
287
+ $DO_CLAUDE && setup_claude_code
288
+ $DO_GEMINI && setup_gemini
289
+ $DO_AIDER && setup_aider
290
+
291
+ echo ""
292
+ echo "╔══════════════════════════════════════════════════════╗"
293
+ echo "║ Setup complete! Quick start: ║"
294
+ echo "║ ║"
295
+ echo "║ Test bridge: python3 integrations/mnemo_bridge.py health"
296
+ echo "║ Get context: integrations/universal/context_inject.sh"
297
+ echo "║ Store memory: integrations/universal/store_session.sh"
298
+ echo "╚══════════════════════════════════════════════════════╝"
299
+ echo ""
integrations/universal/context_inject.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # context_inject.sh — Universal MnemoCore context provider
3
+ # =========================================================
4
+ # Outputs MnemoCore memory context as plain text/markdown.
5
+ # Pipe or include the output into any tool that accepts system prompts.
6
+ #
7
+ # Usage:
8
+ # ./context_inject.sh # General context
9
+ # ./context_inject.sh "bug fix async" # Focused query
10
+ # ./context_inject.sh "" 10 # Top-10 results
11
+ #
12
+ # Examples:
13
+ # codex --system "$(./context_inject.sh)" ...
14
+ # openai-cli --system-prompt "$(./context_inject.sh 'auth')"
15
+
16
+ set -euo pipefail
17
+
18
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
19
+ BRIDGE_PY="${BRIDGE_PY:-$(realpath "$SCRIPT_DIR/../mnemo_bridge.py")}"
20
+
21
+ QUERY="${1:-}"
22
+ TOP_K="${2:-6}"
23
+
24
+ ARGS=(context --top-k "$TOP_K")
25
+ if [[ -n "$QUERY" ]]; then
26
+ ARGS+=(--query "$QUERY")
27
+ fi
28
+
29
+ python3 "$BRIDGE_PY" "${ARGS[@]}" 2>/dev/null || true
integrations/universal/store_session.sh ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # store_session.sh — Store session outcomes into MnemoCore
3
+ # =========================================================
4
+ # Call this at the end of an AI coding session to persist key findings.
5
+ #
6
+ # Usage (interactive):
7
+ # ./store_session.sh
8
+ #
9
+ # Usage (non-interactive / scripted):
10
+ # ./store_session.sh "Fixed race condition in tier_manager.py" "bugfix,async" "my-project"
11
+
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
15
+ BRIDGE_PY="${BRIDGE_PY:-$(realpath "$SCRIPT_DIR/../mnemo_bridge.py")}"
16
+
17
+ if [[ $# -ge 1 ]]; then
18
+ CONTENT="$1"
19
+ TAGS="${2:-cli}"
20
+ CTX="${3:-}"
21
+ else
22
+ echo "Enter memory content (what was done/decided/fixed):"
23
+ read -r CONTENT
24
+ echo "Tags (comma-separated, e.g. bugfix,python,auth):"
25
+ read -r TAGS
26
+ echo "Context/project ID (optional, press Enter to skip):"
27
+ read -r CTX
28
+ fi
29
+
30
+ if [[ -z "$CONTENT" ]]; then
31
+ echo "No content provided, nothing stored." >&2
32
+ exit 0
33
+ fi
34
+
35
+ ARGS=(store "$CONTENT" --source "manual-session" --tags "$TAGS")
36
+ if [[ -n "$CTX" ]]; then
37
+ ARGS+=(--ctx "$CTX")
38
+ fi
39
+
40
+ python3 "$BRIDGE_PY" "${ARGS[@]}"
mnemocore_verify.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import shutil
4
+ import pytest
5
+ from pathlib import Path
6
+ import numpy as np
7
+
8
+ # Set dummy test config environment
9
+ os.environ["HAIM_API_KEY"] = "test-key"
10
+
11
+ from mnemocore.core.config import HAIMConfig, PathsConfig, TierConfig
12
+ from mnemocore.core.binary_hdv import TextEncoder, BinaryHDV
13
+ from mnemocore.core.hnsw_index import HNSWIndexManager
14
+ from mnemocore.core.engine import HAIMEngine
15
+ from mnemocore.core.tier_manager import TierManager
16
+ from unittest.mock import patch
17
+
18
+ @pytest.fixture(autouse=True)
19
+ def setup_test_env():
20
+ # Force all components to use test_data_verify as their data dir
21
+ # to prevent polluting/reading the user's real ./data folder
22
+ test_dir = Path("./test_data_verify")
23
+ test_dir.mkdir(exist_ok=True)
24
+
25
+ cfg = HAIMConfig(
26
+ paths=PathsConfig(
27
+ data_dir=str(test_dir),
28
+ warm_mmap_dir=str(test_dir / "warm"),
29
+ cold_archive_dir=str(test_dir / "cold")
30
+ )
31
+ )
32
+
33
+ with patch('mnemocore.core.config.get_config', return_value=cfg), \
34
+ patch('mnemocore.core.hnsw_index.get_config', return_value=cfg), \
35
+ patch('mnemocore.core.engine.get_config', return_value=cfg):
36
+ yield cfg
37
+
38
+ if test_dir.exists():
39
+ shutil.rmtree(test_dir)
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_text_encoder_normalization():
44
+ """Verify BUG-02: Text normalization fixes identical string variances"""
45
+ encoder = TextEncoder(dimension=1024)
46
+ hdv1 = encoder.encode("Hello World")
47
+ hdv2 = encoder.encode("hello, world!")
48
+
49
+ assert (hdv1.data == hdv2.data).all(), "Normalization failed: Different HDVs for identical texts"
50
+
51
+ def test_hnsw_singleton():
52
+ """Verify BUG-08: HNSWIndexManager is a thread-safe singleton"""
53
+ HNSWIndexManager._instance = None
54
+ idx1 = HNSWIndexManager(dimension=1024)
55
+ idx2 = HNSWIndexManager(dimension=1024)
56
+ assert idx1 is idx2, "HNSWIndexManager is not a singleton"
57
+
58
+ def test_hnsw_index_add_search():
59
+ """Verify BUG-01 & BUG-03: Vector cache lost / Position mapping"""
60
+ HNSWIndexManager._instance = None
61
+ idx = HNSWIndexManager(dimension=1024)
62
+
63
+ # Optional cleanup if it's reused
64
+ idx._id_map = []
65
+ idx._vector_store = []
66
+ if idx._index:
67
+ idx._index.reset()
68
+
69
+ vec1 = BinaryHDV.random(1024)
70
+ vec2 = BinaryHDV.random(1024)
71
+
72
+ idx.add("test_node_1", vec1.data)
73
+ idx.add("test_node_2", vec2.data)
74
+
75
+ assert "test_node_1" in idx._id_map, "ID Map does not contain node 1"
76
+ assert "test_node_2" in idx._id_map, "ID Map does not contain node 2"
77
+
78
+ # The search should return test_node_1 as the top result for vec1.data
79
+ res = idx.search(vec1.data, top_k=1)
80
+ assert res[0][0] == "test_node_1", f"Incorrect search return: {res}"
81
+
82
+ @pytest.mark.asyncio
83
+ async def test_agent_isolation():
84
+ """Verify BUG-09: Agent namespace isolation via engine and tier manager"""
85
+ HNSWIndexManager._instance = None
86
+
87
+ test_data_dir = Path("./test_data_verify")
88
+ test_data_dir.mkdir(exist_ok=True)
89
+
90
+ config = HAIMConfig(
91
+ qdrant=None,
92
+ paths=PathsConfig(
93
+ data_dir=str(test_data_dir),
94
+ warm_mmap_dir=str(test_data_dir / "warm"),
95
+ cold_archive_dir=str(test_data_dir / "cold")
96
+ ),
97
+ tiers_hot=TierConfig(max_memories=1000, ltp_threshold_min=0.0)
98
+ )
99
+ # Prevent newly created memories (LTP=0.5) from being eagerly demoted
100
+ # We run purely local/in-memory for this unit test
101
+
102
+ tier_manager = TierManager(config=config, qdrant_store=None)
103
+ engine = HAIMEngine(
104
+ persist_path=str(test_data_dir / "memory.jsonl"),
105
+ config=config,
106
+ tier_manager=tier_manager
107
+ )
108
+
109
+ try:
110
+ await engine.initialize()
111
+
112
+ # Store two memories, isolated
113
+ await engine.store("Secret logic for agent 1", metadata={"agent_id": "agent_alpha"})
114
+ await engine.store("Public logic for agent 2", metadata={"agent_id": "agent_beta"})
115
+
116
+ # Search global
117
+ res_global = await engine.query("logic", top_k=5)
118
+ # We expect 2 given we just pushed 2
119
+ assert len(res_global) >= 2, f"Global search should return at least 2 memories, got {len(res_global)}"
120
+
121
+ # Search isolated by agent_alpha
122
+ res_isolated = await engine.query("logic", top_k=5, metadata_filter={"agent_id": "agent_alpha"})
123
+
124
+ assert len(res_isolated) > 0, "Should find at least 1 memory for agent_alpha"
125
+ for nid, score in res_isolated:
126
+ node = await engine.get_memory(nid)
127
+ assert node.metadata.get("agent_id") == "agent_alpha", "Found leaked memory from another agent namespace!"
128
+
129
+ finally:
130
+ await engine.close()
131
+ # Clean up test dir
132
+ if test_data_dir.exists():
133
+ shutil.rmtree(test_data_dir)
134
+
135
+ if __name__ == "__main__":
136
+ pytest.main(["-v", __file__])
src/mnemocore/agent_interface.py ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cognitive Memory Client
3
+ =======================
4
+ The high-level facade for autonomous agents to interact with the MnemoCore AGI Memory Substrate.
5
+ Provides easy methods for observation, episodic sequence tracking, and working memory recall.
6
+ """
7
+
8
+ from typing import List, Optional, Any, Tuple
9
+ import logging
10
+
11
+ from .core.engine import HAIMEngine
12
+ from .core.working_memory import WorkingMemoryService, WorkingMemoryItem
13
+ from .core.episodic_store import EpisodicStoreService
14
+ from .core.semantic_store import SemanticStoreService
15
+ from .core.procedural_store import ProceduralStoreService
16
+ from .core.meta_memory import MetaMemoryService, SelfImprovementProposal
17
+ from .core.memory_model import Procedure
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ class CognitiveMemoryClient:
22
+ """
23
+ Plug-and-play cognitive memory facade for agent frameworks (LangGraph, AutoGen, OpenClaw, etc.).
24
+ """
25
+ def __init__(
26
+ self,
27
+ engine: HAIMEngine,
28
+ wm: WorkingMemoryService,
29
+ episodic: EpisodicStoreService,
30
+ semantic: SemanticStoreService,
31
+ procedural: ProceduralStoreService,
32
+ meta: MetaMemoryService,
33
+ ):
34
+ self.engine = engine
35
+ self.wm = wm
36
+ self.episodic = episodic
37
+ self.semantic = semantic
38
+ self.procedural = procedural
39
+ self.meta = meta
40
+
41
+ # --- Observation & WM ---
42
+
43
+ def observe(self, agent_id: str, content: str, kind: str = "observation", importance: float = 0.5, tags: Optional[List[str]] = None, **meta) -> str:
44
+ """
45
+ Push a new observation or thought directly into the agent's short-term Working Memory.
46
+ """
47
+ import uuid
48
+ from datetime import datetime
49
+ item_id = f"wm_{uuid.uuid4().hex[:8]}"
50
+
51
+ item = WorkingMemoryItem(
52
+ id=item_id,
53
+ agent_id=agent_id,
54
+ created_at=datetime.utcnow(),
55
+ ttl_seconds=3600, # 1 hour default
56
+ content=content,
57
+ kind=kind, # type: ignore
58
+ importance=importance,
59
+ tags=tags or [],
60
+ hdv=None # Could encode via engine in future
61
+ )
62
+ self.wm.push_item(agent_id, item)
63
+ logger.debug(f"Agent {agent_id} observed: {content[:30]}...")
64
+ return item_id
65
+
66
+ def get_working_context(self, agent_id: str, limit: int = 16) -> List[WorkingMemoryItem]:
67
+ """
68
+ Read the active, un-pruned context out of the agent's working memory buffer.
69
+ """
70
+ state = self.wm.get_state(agent_id)
71
+ if not state:
72
+ return []
73
+
74
+ return state.items[-limit:]
75
+
76
+ # --- Episodic ---
77
+
78
+ def start_episode(self, agent_id: str, goal: str, context: Optional[str] = None) -> str:
79
+ """Begin a new temporally-linked event sequence."""
80
+ return self.episodic.start_episode(agent_id, goal=goal, context=context)
81
+
82
+ def append_event(self, episode_id: str, kind: str, content: str, **meta) -> None:
83
+ """Log an action or outcome to an ongoing episode."""
84
+ self.episodic.append_event(episode_id, kind, content, meta)
85
+
86
+ def end_episode(self, episode_id: str, outcome: str, reward: Optional[float] = None) -> None:
87
+ """Seal an episode, logging its final success or failure state."""
88
+ self.episodic.end_episode(episode_id, outcome, reward)
89
+
90
+ # --- Semantic / Retrieval ---
91
+
92
+ async def recall(
93
+ self,
94
+ agent_id: str,
95
+ query: str,
96
+ context: Optional[str] = None,
97
+ top_k: int = 8,
98
+ modes: Tuple[str, ...] = ("episodic", "semantic")
99
+ ) -> List[dict]:
100
+ """
101
+ A unified query interface that checks Working Memory, Episodic History, and the Semantic Vector Store.
102
+ Currently delegates heavily to the backing HAIMEngine, but can be augmented to return semantic concepts.
103
+ """
104
+ results = []
105
+
106
+ # 1. Broad retrieval via existing HAIM engine (SM / general memories)
107
+ if "semantic" in modes:
108
+ engine_results = await self.engine.query(query, top_k=top_k)
109
+ for mem_id, score in engine_results:
110
+ node = await self.engine.tier_manager.get_memory(mem_id) # Fix: tier_manager.get_memory is async
111
+ if node:
112
+ results.append({"source": "semantic/engine", "content": node.content, "score": score})
113
+
114
+ # 2. Local episodic retrieval
115
+ if "episodic" in modes:
116
+ recent_eps = self.episodic.get_recent(agent_id, limit=top_k, context=context)
117
+ for ep in recent_eps:
118
+ summary = f"Episode(goal={ep.goal}, outcome={ep.outcome}, events={len(ep.events)})"
119
+ results.append({"source": "episodic", "content": summary, "score": ep.reliability})
120
+
121
+ # Sort and trim mixed results
122
+ results.sort(key=lambda x: x.get("score", 0.0), reverse=True)
123
+ return results[:top_k]
124
+
125
+ # --- Procedural ---
126
+
127
+ def suggest_procedures(self, agent_id: str, query: str, top_k: int = 5) -> List[Procedure]:
128
+ """Fetch executable tool-patterns based on the agent's intent."""
129
+ return self.procedural.find_applicable_procedures(query, agent_id=agent_id, top_k=top_k)
130
+
131
+ def record_procedure_outcome(self, proc_id: str, success: bool) -> None:
132
+ """Report on the utility of a chosen procedure."""
133
+ self.procedural.record_procedure_outcome(proc_id, success)
134
+
135
+ # --- Meta / Self-awareness ---
136
+
137
+ def get_knowledge_gaps(self, agent_id: str, lookback_hours: int = 24) -> List[dict]:
138
+ """Return currently open knowledge gaps identified by the Pulse loop."""
139
+ # Stubbed: Would interact with gap_detector
140
+ return []
141
+
142
+ def get_self_improvement_proposals(self) -> List[SelfImprovementProposal]:
143
+ """Retrieve system-generated proposals to improve operation or prompt alignment."""
144
+ return self.meta.list_proposals()
145
+
src/mnemocore/api/main.py CHANGED
@@ -156,9 +156,22 @@ async def lifespan(app: FastAPI):
156
  persist_path="./data/memory.jsonl",
157
  config=config,
158
  tier_manager=tier_manager,
 
 
 
159
  )
160
  await engine.initialize()
161
  app.state.engine = engine
 
 
 
 
 
 
 
 
 
 
162
 
163
  yield
164
 
@@ -377,7 +390,8 @@ async def query_memory(
377
  API_REQUEST_COUNT.labels(method="POST", endpoint="/query", status="200").inc()
378
 
379
  # CPU heavy vector search (offloaded inside engine)
380
- results = await engine.query(req.query, top_k=req.top_k)
 
381
 
382
  formatted = []
383
  for mem_id, score in results:
@@ -476,6 +490,52 @@ async def delete_memory(
476
 
477
  return {"ok": True, "deleted": memory_id}
478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  # --- Conceptual Endpoints ---
480
 
481
  @app.post(
@@ -627,6 +687,310 @@ async def rlm_query(
627
  }
628
 
629
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  if __name__ == "__main__":
631
  import uvicorn
632
  uvicorn.run(app, host="0.0.0.0", port=8100)
 
156
  persist_path="./data/memory.jsonl",
157
  config=config,
158
  tier_manager=tier_manager,
159
+ working_memory=container.working_memory,
160
+ episodic_store=container.episodic_store,
161
+ semantic_store=container.semantic_store,
162
  )
163
  await engine.initialize()
164
  app.state.engine = engine
165
+ # Also expose the cognitive client to app state for agentic frameworks
166
+ from mnemocore.agent_interface import CognitiveMemoryClient
167
+ app.state.cognitive_client = CognitiveMemoryClient(
168
+ engine=engine,
169
+ wm=container.working_memory,
170
+ episodic=container.episodic_store,
171
+ semantic=container.semantic_store,
172
+ procedural=container.procedural_store,
173
+ meta=container.meta_memory,
174
+ )
175
 
176
  yield
177
 
 
390
  API_REQUEST_COUNT.labels(method="POST", endpoint="/query", status="200").inc()
391
 
392
  # CPU heavy vector search (offloaded inside engine)
393
+ metadata_filter = {"agent_id": req.agent_id} if req.agent_id else None
394
+ results = await engine.query(req.query, top_k=req.top_k, metadata_filter=metadata_filter)
395
 
396
  formatted = []
397
  for mem_id, score in results:
 
490
 
491
  return {"ok": True, "deleted": memory_id}
492
 
493
+ # --- Phase 5: Cognitive Client Endpoints ---
494
+
495
+ class ObserveRequest(BaseModel):
496
+ agent_id: str
497
+ content: str
498
+ kind: str = "observation"
499
+ importance: float = 0.5
500
+ tags: Optional[List[str]] = None
501
+
502
+ @app.post("/wm/observe", dependencies=[Depends(get_api_key)])
503
+ async def observe_context(req: ObserveRequest, request: Request):
504
+ """Push an observation explicitly into Working Memory."""
505
+ client = request.app.state.cognitive_client
506
+ if not client:
507
+ raise HTTPException(status_code=503, detail="Cognitive Client unavailable")
508
+ item_id = client.observe(
509
+ agent_id=req.agent_id,
510
+ content=req.content,
511
+ kind=req.kind,
512
+ importance=req.importance,
513
+ tags=req.tags
514
+ )
515
+ return {"ok": True, "item_id": item_id}
516
+
517
+ @app.get("/wm/context/{agent_id}", dependencies=[Depends(get_api_key)])
518
+ async def get_working_context(agent_id: str, limit: int = 16, request: Request = None):
519
+ """Read active Working Memory context."""
520
+ client = request.app.state.cognitive_client
521
+ items = client.get_working_context(agent_id, limit=limit)
522
+ return {"ok": True, "items": [
523
+ {"id": i.id, "content": i.content, "kind": i.kind, "importance": i.importance}
524
+ for i in items
525
+ ]}
526
+
527
+ class EpisodeStartRequest(BaseModel):
528
+ agent_id: str
529
+ goal: str
530
+ context: Optional[str] = None
531
+
532
+ @app.post("/episodes/start", dependencies=[Depends(get_api_key)])
533
+ async def start_episode(req: EpisodeStartRequest, request: Request):
534
+ """Start a new episode chain."""
535
+ client = request.app.state.cognitive_client
536
+ ep_id = client.start_episode(req.agent_id, goal=req.goal, context=req.context)
537
+ return {"ok": True, "episode_id": ep_id}
538
+
539
  # --- Conceptual Endpoints ---
540
 
541
  @app.post(
 
687
  }
688
 
689
 
690
+ # ─────────────────────────────────────────────────────────────────────────────
691
+ # Phase 5.0 — Agent 1: Trust & Provenance Endpoints
692
+ # ─────────────────────────────────────────────────────────────────────────────
693
+
694
+ @app.get(
695
+ "/memory/{memory_id}/lineage",
696
+ dependencies=[Depends(get_api_key)],
697
+ tags=["Phase 5.0 — Trust"],
698
+ summary="Get full provenance lineage for a memory",
699
+ )
700
+ async def get_memory_lineage(
701
+ memory_id: str,
702
+ engine: HAIMEngine = Depends(get_engine),
703
+ ):
704
+ """
705
+ Return the complete provenance lineage of a memory:
706
+ origin (who created it, how, when) and all transformation events
707
+ (consolidated, verified, contradicted, archived, …).
708
+ """
709
+ node = await engine.get_memory(memory_id)
710
+ if not node:
711
+ raise MemoryNotFoundError(memory_id)
712
+
713
+ prov = getattr(node, "provenance", None)
714
+ if prov is None:
715
+ return {
716
+ "ok": True,
717
+ "memory_id": memory_id,
718
+ "provenance": None,
719
+ "message": "No provenance record attached to this memory.",
720
+ }
721
+
722
+ return {
723
+ "ok": True,
724
+ "memory_id": memory_id,
725
+ "provenance": prov.to_dict(),
726
+ }
727
+
728
+
729
+ @app.get(
730
+ "/memory/{memory_id}/confidence",
731
+ dependencies=[Depends(get_api_key)],
732
+ tags=["Phase 5.0 — Trust"],
733
+ summary="Get confidence envelope for a memory",
734
+ )
735
+ async def get_memory_confidence(
736
+ memory_id: str,
737
+ engine: HAIMEngine = Depends(get_engine),
738
+ ):
739
+ """
740
+ Return a structured confidence envelope for a memory, combining:
741
+ - Bayesian reliability (BayesianLTP posterior mean)
742
+ - access_count (evidence strength)
743
+ - staleness (days since last verification)
744
+ - source_type trust weight
745
+ - contradiction flag
746
+
747
+ Level: high | medium | low | contradicted | stale
748
+ """
749
+ from mnemocore.core.confidence import build_confidence_envelope
750
+
751
+ node = await engine.get_memory(memory_id)
752
+ if not node:
753
+ raise MemoryNotFoundError(memory_id)
754
+
755
+ prov = getattr(node, "provenance", None)
756
+ envelope = build_confidence_envelope(node, prov)
757
+
758
+ return {
759
+ "ok": True,
760
+ "memory_id": memory_id,
761
+ "confidence": envelope,
762
+ }
763
+
764
+
765
+ # ─────────────────────────────────────────────────────────────────────────────
766
+ # Phase 5.0 — Agent 3 stub: Proactive Recall
767
+ # (Full implementation added by Agent 3 workstream)
768
+ # ─────────────────────────────────────────────────────────────────────────────
769
+
770
+ @app.get(
771
+ "/proactive",
772
+ dependencies=[Depends(get_api_key)],
773
+ tags=["Phase 5.0 — Autonomy"],
774
+ summary="Retrieve contextually relevant memories without explicit query",
775
+ )
776
+ async def get_proactive_memories(
777
+ agent_id: Optional[str] = None,
778
+ limit: int = 10,
779
+ engine: HAIMEngine = Depends(get_engine),
780
+ ):
781
+ """
782
+ Proactive recall stub (Phase 5.0 / Agent 3).
783
+ Returns the most recently active high-LTP memories as a stand-in
784
+ until the full ProactiveRecallDaemon is implemented.
785
+ """
786
+ nodes = await engine.tier_manager.get_hot_snapshot() if hasattr(engine, "tier_manager") else []
787
+ sorted_nodes = sorted(nodes, key=lambda n: n.ltp_strength, reverse=True)[:limit]
788
+
789
+ from mnemocore.core.confidence import build_confidence_envelope
790
+ results = []
791
+ for n in sorted_nodes:
792
+ prov = getattr(n, "provenance", None)
793
+ results.append({
794
+ "id": n.id,
795
+ "content": n.content,
796
+ "ltp_strength": round(n.ltp_strength, 4),
797
+ "confidence": build_confidence_envelope(n, prov),
798
+ "tier": getattr(n, "tier", "hot"),
799
+ })
800
+
801
+ return {"ok": True, "proactive_results": results, "count": len(results)}
802
+
803
+
804
+ # ─────────────────────────────────────────────────────────────────────────────
805
+ # Phase 5.0 — Agent 2: Memory Lifecycle Endpoints
806
+ # ─────────────────────────────────────────────────────────────────────────────
807
+
808
+ @app.get(
809
+ "/contradictions",
810
+ dependencies=[Depends(get_api_key)],
811
+ tags=["Phase 5.0 — Lifecycle"],
812
+ summary="List active contradiction groups requiring resolution",
813
+ )
814
+ async def list_contradictions(
815
+ unresolved_only: bool = True,
816
+ ):
817
+ """
818
+ Returns all detected contradiction groups from the ContradictionRegistry.
819
+ By default only unresolved contradictions are returned.
820
+ """
821
+ from mnemocore.core.contradiction import get_contradiction_detector
822
+ detector = get_contradiction_detector()
823
+ records = detector.registry.list_all(unresolved_only=unresolved_only)
824
+ return {
825
+ "ok": True,
826
+ "count": len(records),
827
+ "contradictions": [r.to_dict() for r in records],
828
+ }
829
+
830
+
831
+ class ResolveContradictionRequest(BaseModel):
832
+ note: Optional[str] = None
833
+
834
+
835
+ @app.post(
836
+ "/contradictions/{group_id}/resolve",
837
+ dependencies=[Depends(get_api_key)],
838
+ tags=["Phase 5.0 — Lifecycle"],
839
+ summary="Mark a contradiction group as resolved",
840
+ )
841
+ async def resolve_contradiction(group_id: str, req: ResolveContradictionRequest):
842
+ """Manually resolve a detected contradiction."""
843
+ from mnemocore.core.contradiction import get_contradiction_detector
844
+ detector = get_contradiction_detector()
845
+ success = detector.registry.resolve(group_id, note=req.note)
846
+ if not success:
847
+ raise HTTPException(status_code=404, detail=f"Contradiction group {group_id!r} not found.")
848
+ return {"ok": True, "resolved_group_id": group_id}
849
+
850
+
851
+ # ─────────────────────────────────────────────────────────────────────────────
852
+ # Phase 5.0 — Agent 3: Autonomous Cognition Endpoints
853
+ # ───────────��─────────────────────────────────────────────────────────────────
854
+
855
+ @app.get(
856
+ "/memory/{memory_id}/emotional-tag",
857
+ dependencies=[Depends(get_api_key)],
858
+ tags=["Phase 5.0 — Autonomy"],
859
+ summary="Get emotional (valence/arousal) tag for a memory",
860
+ )
861
+ async def get_emotional_tag_ep(
862
+ memory_id: str,
863
+ engine: HAIMEngine = Depends(get_engine),
864
+ ):
865
+ """Return the valence/arousal emotional metadata for a memory."""
866
+ from mnemocore.core.emotional_tag import get_emotional_tag
867
+ node = await engine.get_memory(memory_id)
868
+ if not node:
869
+ raise MemoryNotFoundError(memory_id)
870
+ tag = get_emotional_tag(node)
871
+ return {
872
+ "ok": True,
873
+ "memory_id": memory_id,
874
+ "emotional_tag": {
875
+ "valence": tag.valence,
876
+ "arousal": tag.arousal,
877
+ "salience": round(tag.salience(), 4),
878
+ },
879
+ }
880
+
881
+
882
+ class EmotionalTagPatchRequest(BaseModel):
883
+ valence: float
884
+ arousal: float
885
+
886
+
887
+ @app.patch(
888
+ "/memory/{memory_id}/emotional-tag",
889
+ dependencies=[Depends(get_api_key)],
890
+ tags=["Phase 5.0 — Autonomy"],
891
+ summary="Attach or update emotional tag on a memory",
892
+ )
893
+ async def patch_emotional_tag(
894
+ memory_id: str,
895
+ req: EmotionalTagPatchRequest,
896
+ engine: HAIMEngine = Depends(get_engine),
897
+ ):
898
+ from mnemocore.core.emotional_tag import EmotionalTag, attach_emotional_tag
899
+ node = await engine.get_memory(memory_id)
900
+ if not node:
901
+ raise MemoryNotFoundError(memory_id)
902
+ tag = EmotionalTag(valence=req.valence, arousal=req.arousal)
903
+ attach_emotional_tag(node, tag)
904
+ return {"ok": True, "memory_id": memory_id, "emotional_tag": tag.to_metadata_dict()}
905
+
906
+
907
+ # ─────────────────────────────────────────────────────────────────────────────
908
+ # Phase 5.0 — Agent 4: Prediction Endpoints
909
+ # ─────────────────────────────────────────────────────────────────────────────
910
+
911
+ _prediction_store_instance = None
912
+
913
+
914
+ def _get_prediction_store(engine: HAIMEngine = Depends(get_engine)):
915
+ from mnemocore.core.prediction_store import PredictionStore
916
+ global _prediction_store_instance
917
+ if _prediction_store_instance is None:
918
+ _prediction_store_instance = PredictionStore(engine=engine)
919
+ return _prediction_store_instance
920
+
921
+
922
+ class CreatePredictionRequest(BaseModel):
923
+ content: str
924
+ confidence: float = 0.5
925
+ deadline_days: Optional[float] = None
926
+ related_memory_ids: Optional[List[str]] = None
927
+ tags: Optional[List[str]] = None
928
+
929
+
930
+ class VerifyPredictionRequest(BaseModel):
931
+ success: bool
932
+ notes: Optional[str] = None
933
+
934
+
935
+ @app.post(
936
+ "/predictions",
937
+ dependencies=[Depends(get_api_key)],
938
+ tags=["Phase 5.0 — Prediction"],
939
+ summary="Store a new forward-looking prediction",
940
+ )
941
+ async def create_prediction(req: CreatePredictionRequest):
942
+ from mnemocore.core.prediction_store import PredictionStore
943
+ global _prediction_store_instance
944
+ if _prediction_store_instance is None:
945
+ _prediction_store_instance = PredictionStore()
946
+ pred_id = _prediction_store_instance.create(
947
+ content=req.content,
948
+ confidence=req.confidence,
949
+ deadline_days=req.deadline_days,
950
+ related_memory_ids=req.related_memory_ids,
951
+ tags=req.tags,
952
+ )
953
+ pred = _prediction_store_instance.get(pred_id)
954
+ return {"ok": True, "prediction": pred.to_dict()}
955
+
956
+
957
+ @app.get(
958
+ "/predictions",
959
+ dependencies=[Depends(get_api_key)],
960
+ tags=["Phase 5.0 — Prediction"],
961
+ summary="List all predictions",
962
+ )
963
+ async def list_predictions(status: Optional[str] = None):
964
+ from mnemocore.core.prediction_store import PredictionStore
965
+ global _prediction_store_instance
966
+ if _prediction_store_instance is None:
967
+ _prediction_store_instance = PredictionStore()
968
+ return {
969
+ "ok": True,
970
+ "predictions": [
971
+ p.to_dict()
972
+ for p in _prediction_store_instance.list_all(status=status)
973
+ ],
974
+ }
975
+
976
+
977
+ @app.post(
978
+ "/predictions/{pred_id}/verify",
979
+ dependencies=[Depends(get_api_key)],
980
+ tags=["Phase 5.0 — Prediction"],
981
+ summary="Verify or falsify a prediction",
982
+ )
983
+ async def verify_prediction(pred_id: str, req: VerifyPredictionRequest):
984
+ from mnemocore.core.prediction_store import PredictionStore
985
+ global _prediction_store_instance
986
+ if _prediction_store_instance is None:
987
+ _prediction_store_instance = PredictionStore()
988
+ pred = await _prediction_store_instance.verify(pred_id, success=req.success, notes=req.notes)
989
+ if pred is None:
990
+ raise HTTPException(status_code=404, detail=f"Prediction {pred_id!r} not found.")
991
+ return {"ok": True, "prediction": pred.to_dict()}
992
+
993
+
994
  if __name__ == "__main__":
995
  import uvicorn
996
  uvicorn.run(app, host="0.0.0.0", port=8100)
src/mnemocore/core/agent_profile.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Agent Profiles
3
+ ==============
4
+ Persistent state encompassing quirks, long-term alignment details, and tooling preferences per individual actor.
5
+ Allows multiple independent agents to interact cleanly without memory namespace collisions.
6
+ """
7
+
8
+ from typing import Dict, List, Optional, Any
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ import threading
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ @dataclass
17
+ class AgentProfile:
18
+ id: str
19
+ name: str
20
+ description: str
21
+ created_at: datetime
22
+ last_active: datetime
23
+ # Hard bounds over behavior: e.g. "Do not delete files without explicit prompt"
24
+ core_directives: List[str] = field(default_factory=list)
25
+ # Flexible learned preferences
26
+ preferences: dict[str, Any] = field(default_factory=dict)
27
+ # Agent-specific metrics
28
+ reliability_score: float = 1.0
29
+
30
+
31
+ class AgentProfileService:
32
+ def __init__(self):
33
+ # Local state dict, should back out to SQLite or Redis
34
+ self._profiles: Dict[str, AgentProfile] = {}
35
+ self._lock = threading.RLock()
36
+
37
+ def get_or_create_profile(self, agent_id: str, name: str = "Unknown Agent") -> AgentProfile:
38
+ """Retrieve the identity profile for an agent, constructing it if completely uninitialized."""
39
+ with self._lock:
40
+ if agent_id not in self._profiles:
41
+ self._profiles[agent_id] = AgentProfile(
42
+ id=agent_id,
43
+ name=name,
44
+ description=f"Auto-generated profile for {agent_id}",
45
+ created_at=datetime.utcnow(),
46
+ last_active=datetime.utcnow()
47
+ )
48
+
49
+ profile = self._profiles[agent_id]
50
+ profile.last_active = datetime.utcnow()
51
+ return profile
52
+
53
+ def update_preferences(self, agent_id: str, new_preferences: dict[str, Any]) -> None:
54
+ """Merge learned trait or task preferences into an agent's persistent identity."""
55
+ with self._lock:
56
+ profile = self.get_or_create_profile(agent_id)
57
+ profile.preferences.update(new_preferences)
58
+ logger.debug(f"Updated preferences for agent {agent_id}.")
59
+
60
+ def adjust_reliability(self, agent_id: str, points: float) -> None:
61
+ """Alter universal trust rating of the agent based on episodic action evaluations."""
62
+ with self._lock:
63
+ profile = self.get_or_create_profile(agent_id)
64
+ profile.reliability_score = max(0.0, min(1.0, profile.reliability_score + points))
65
+
src/mnemocore/core/anticipatory.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from loguru import logger
3
+
4
+ from .config import AnticipatoryConfig
5
+ from .synapse_index import SynapseIndex
6
+ from .tier_manager import TierManager
7
+ from .topic_tracker import TopicTracker
8
+
9
+ class AnticipatoryEngine:
10
+ """
11
+ Phase 13.2: Anticipatory Memory
12
+ Predicts which memories the user is likely to request next based on the
13
+ current topic trajectory and graph structure, and pre-loads them into the HOT tier.
14
+ """
15
+ def __init__(
16
+ self,
17
+ config: AnticipatoryConfig,
18
+ synapse_index: SynapseIndex,
19
+ tier_manager: TierManager,
20
+ topic_tracker: TopicTracker
21
+ ):
22
+ self.config = config
23
+ self.synapse_index = synapse_index
24
+ self.tier_manager = tier_manager
25
+ self.topic_tracker = topic_tracker
26
+
27
+ async def predict_and_preload(self, current_node_id: str) -> List[str]:
28
+ """
29
+ Predicts surrounding context from the current node and ensures they are preloaded.
30
+ Uses the multi-hop network in the SynapseIndex to find likely next nodes.
31
+ """
32
+ if not self.config.enabled:
33
+ return []
34
+
35
+ # Get neighbors up to predictive depth
36
+ # We use a relatively low depth to avoid flooding the HOT tier
37
+ neighbors = self.synapse_index.get_multi_hop_neighbors(
38
+ current_node_id,
39
+ depth=self.config.predictive_depth
40
+ )
41
+
42
+ # We'll just take the top 5 highest-weighted neighbors
43
+ # Sort by path weight (which multi-hop computes)
44
+ sorted_neighbors = sorted(neighbors.items(), key=lambda x: x[1], reverse=True)[:5]
45
+ target_ids = [nid for nid, weight in sorted_neighbors if nid != current_node_id]
46
+
47
+ if target_ids:
48
+ logger.debug(f"Anticipatory engine pre-loading {len(target_ids)} predicted nodes.")
49
+ await self.tier_manager.anticipate(target_ids)
50
+
51
+ return target_ids
src/mnemocore/core/binary_hdv.py CHANGED
@@ -22,6 +22,21 @@ import hashlib
22
  from typing import List, Optional, Tuple
23
 
24
  import numpy as np
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
 
27
  class BinaryHDV:
@@ -138,12 +153,13 @@ class BinaryHDV:
138
  """
139
  Hamming distance: count of differing bits.
140
 
141
- Uses np.unpackbits + sum for correctness.
142
  Range: [0, dimension].
143
  """
144
  assert self.dimension == other.dimension
145
  xor_result = np.bitwise_xor(self.data, other.data)
146
- return int(np.unpackbits(xor_result).sum())
 
147
 
148
  def normalized_distance(self, other: "BinaryHDV") -> float:
149
  """Hamming distance normalized to [0.0, 1.0]."""
@@ -210,7 +226,8 @@ class BinaryHDV:
210
  return cls(data=data, dimension=dimension)
211
 
212
  def __repr__(self) -> str:
213
- popcount = int(np.unpackbits(self.data).sum())
 
214
  return f"BinaryHDV(dim={self.dimension}, popcount={popcount}/{self.dimension})"
215
 
216
  def __eq__(self, other: object) -> bool:
@@ -383,26 +400,37 @@ class TextEncoder:
383
  """
384
  Encode a text string to a binary HDV.
385
 
386
- Tokenization: simple whitespace split + lowercasing.
387
  Each token is bound with its position via XOR(token, permute(position_marker, i)).
388
  All position-bound tokens are bundled via majority vote.
389
  """
390
- tokens = text.lower().split()
 
 
391
  if not tokens:
392
  return BinaryHDV.random(self.dimension)
393
 
394
  if len(tokens) == 1:
395
  return self.get_token_vector(tokens[0])
396
 
397
- # Build position-bound token vectors
398
- bound_vectors = []
399
- for i, token in enumerate(tokens):
400
- token_hdv = self.get_token_vector(token)
401
- # Permute by position index for order encoding
402
- positioned = token_hdv.permute(shift=i)
403
- bound_vectors.append(positioned)
 
 
 
404
 
405
- return majority_bundle(bound_vectors)
 
 
 
 
 
 
406
 
407
  def encode_with_context(
408
  self, text: str, context_hdv: BinaryHDV
@@ -415,21 +443,3 @@ class TextEncoder:
415
  """
416
  content_hdv = self.encode(text)
417
  return content_hdv.xor_bind(context_hdv)
418
-
419
-
420
- # ======================================================================
421
- # Internal helpers
422
- # ======================================================================
423
-
424
- # Cached lookup table for popcount (bits set per byte value 0-255)
425
- _POPCOUNT_TABLE: Optional[np.ndarray] = None
426
-
427
-
428
- def _build_popcount_table() -> np.ndarray:
429
- """Build or return cached popcount lookup table for bytes (0-255)."""
430
- global _POPCOUNT_TABLE
431
- if _POPCOUNT_TABLE is None:
432
- _POPCOUNT_TABLE = np.array(
433
- [bin(i).count("1") for i in range(256)], dtype=np.int32
434
- )
435
- return _POPCOUNT_TABLE
 
22
  from typing import List, Optional, Tuple
23
 
24
  import numpy as np
25
+ import re
26
+
27
+
28
+ # Cached lookup table for popcount (bits set per byte value 0-255)
29
+ _POPCOUNT_TABLE: Optional[np.ndarray] = None
30
+
31
+
32
+ def _build_popcount_table() -> np.ndarray:
33
+ """Build or return cached popcount lookup table for bytes (0-255)."""
34
+ global _POPCOUNT_TABLE
35
+ if _POPCOUNT_TABLE is None:
36
+ _POPCOUNT_TABLE = np.array(
37
+ [bin(i).count("1") for i in range(256)], dtype=np.int32
38
+ )
39
+ return _POPCOUNT_TABLE
40
 
41
 
42
  class BinaryHDV:
 
153
  """
154
  Hamming distance: count of differing bits.
155
 
156
+ Uses lookup table for speed (replacing unpackbits).
157
  Range: [0, dimension].
158
  """
159
  assert self.dimension == other.dimension
160
  xor_result = np.bitwise_xor(self.data, other.data)
161
+ # Optimized: use precomputed popcount table instead of unpacking bits
162
+ return int(_build_popcount_table()[xor_result].sum())
163
 
164
  def normalized_distance(self, other: "BinaryHDV") -> float:
165
  """Hamming distance normalized to [0.0, 1.0]."""
 
226
  return cls(data=data, dimension=dimension)
227
 
228
  def __repr__(self) -> str:
229
+ # Optimized: use precomputed popcount table
230
+ popcount = int(_build_popcount_table()[self.data].sum())
231
  return f"BinaryHDV(dim={self.dimension}, popcount={popcount}/{self.dimension})"
232
 
233
  def __eq__(self, other: object) -> bool:
 
400
  """
401
  Encode a text string to a binary HDV.
402
 
403
+ Tokenization: simple whitespace split after normalization.
404
  Each token is bound with its position via XOR(token, permute(position_marker, i)).
405
  All position-bound tokens are bundled via majority vote.
406
  """
407
+ # BUG-02 Fix: strip punctuation and normalize spaces
408
+ normalized = re.sub(r'[^\w\s]', '', text).lower()
409
+ tokens = normalized.split()
410
  if not tokens:
411
  return BinaryHDV.random(self.dimension)
412
 
413
  if len(tokens) == 1:
414
  return self.get_token_vector(tokens[0])
415
 
416
+ # Build position-bound token vectors (#27)
417
+ # Optimized: Batch process data instead of multiple object instantiations
418
+ token_hdvs = [self.get_token_vector(t) for t in tokens]
419
+ packed_data = np.stack([v.data for v in token_hdvs], axis=0)
420
+ all_bits = np.unpackbits(packed_data, axis=1)
421
+
422
+ # Apply position-based permutations (roll)
423
+ for i in range(len(tokens)):
424
+ if i > 0:
425
+ all_bits[i] = np.roll(all_bits[i], i)
426
 
427
+ # Vectorized majority vote (equivalent to majority_bundle)
428
+ sums = all_bits.sum(axis=0)
429
+ threshold = len(tokens) / 2.0
430
+ result_bits = np.zeros(self.dimension, dtype=np.uint8)
431
+ result_bits[sums > threshold] = 1
432
+
433
+ return BinaryHDV(data=np.packbits(result_bits), dimension=self.dimension)
434
 
435
  def encode_with_context(
436
  self, text: str, context_hdv: BinaryHDV
 
443
  """
444
  content_hdv = self.encode(text)
445
  return content_hdv.xor_bind(context_hdv)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/mnemocore/core/confidence.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Confidence Calibration Module (Phase 5.0)
3
+ ==========================================
4
+ Generates structured confidence envelopes for retrieved memories,
5
+ combining all available trust signals into a single queryable object.
6
+
7
+ Signals used:
8
+ - BayesianLTP reliability (mean of Beta posterior)
9
+ - access_count (low count → less evidence)
10
+ - staleness (days since last verification)
11
+ - source type (external ≤ user_correction vs observation)
12
+ - contradiction flag (from ProvenanceRecord)
13
+
14
+ Output: a ConfidenceEnvelope dict appended to every query response,
15
+ enabling consuming agents to make trust-aware decisions.
16
+
17
+ Public API:
18
+ env = ConfidenceEnvelopeGenerator.build(node, provenance)
19
+ level = env["level"] # "high" | "medium" | "low" | "contradicted" | "stale"
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from datetime import datetime, timezone
25
+ from typing import TYPE_CHECKING, Any, Dict, Optional
26
+
27
+ if TYPE_CHECKING:
28
+ from .node import MemoryNode
29
+ from .provenance import ProvenanceRecord
30
+
31
+
32
+ # ------------------------------------------------------------------ #
33
+ # Confidence levels (ordered by trust) #
34
+ # ------------------------------------------------------------------ #
35
+
36
+ LEVEL_HIGH = "high"
37
+ LEVEL_MEDIUM = "medium"
38
+ LEVEL_LOW = "low"
39
+ LEVEL_CONTRADICTED = "contradicted"
40
+ LEVEL_STALE = "stale"
41
+
42
+ # Thresholds
43
+ RELIABILITY_HIGH_THRESHOLD = 0.80
44
+ RELIABILITY_MEDIUM_THRESHOLD = 0.50
45
+ ACCESS_COUNT_MIN_EVIDENCE = 2 # Less than this → low confidence
46
+ ACCESS_COUNT_HIGH_EVIDENCE = 5 # At least this → supports high confidence
47
+ STALENESS_STALE_DAYS = 30 # Days without verification → stale
48
+
49
+
50
+ # ------------------------------------------------------------------ #
51
+ # Source-type trust weights #
52
+ # ------------------------------------------------------------------ #
53
+
54
+ SOURCE_TRUST: Dict[str, float] = {
55
+ "observation": 1.0,
56
+ "inference": 0.8,
57
+ "external_sync": 0.75,
58
+ "dream": 0.6,
59
+ "consolidation": 0.85,
60
+ "prediction": 0.5,
61
+ "user_correction": 1.0,
62
+ "unknown": 0.5,
63
+ }
64
+
65
+
66
+ # ------------------------------------------------------------------ #
67
+ # Confidence Envelope Generator #
68
+ # ------------------------------------------------------------------ #
69
+
70
+ class ConfidenceEnvelopeGenerator:
71
+ """
72
+ Builds a confidence_envelope dict for a MemoryNode.
73
+
74
+ Does NOT mutate the node — only reads fields.
75
+ Thread-safe; no shared state.
76
+ """
77
+
78
+ @staticmethod
79
+ def _reliability(node: "MemoryNode") -> float:
80
+ """
81
+ Extract reliability float from the node.
82
+ Falls back to ltp_strength if no Bayesian state is attached.
83
+ """
84
+ bayes = getattr(node, "_bayes", None)
85
+ if bayes is not None:
86
+ return float(bayes.mean)
87
+ return float(getattr(node, "ltp_strength", 0.5))
88
+
89
+ @staticmethod
90
+ def _staleness_days(node: "MemoryNode", provenance: Optional["ProvenanceRecord"]) -> float:
91
+ """Days since last verification, or days since last access."""
92
+ if provenance:
93
+ # Find the most recent 'verified' event
94
+ for evt in reversed(provenance.lineage):
95
+ if evt.event == "verified" and evt.outcome is True:
96
+ try:
97
+ ts = datetime.fromisoformat(evt.timestamp)
98
+ if ts.tzinfo is None:
99
+ ts = ts.replace(tzinfo=timezone.utc)
100
+ delta = datetime.now(timezone.utc) - ts
101
+ return delta.total_seconds() / 86400.0
102
+ except (ValueError, TypeError):
103
+ pass
104
+
105
+ # Fall back to last_accessed on the node
106
+ last = getattr(node, "last_accessed", None)
107
+ if last is not None:
108
+ if getattr(last, "tzinfo", None) is None:
109
+ last = last.replace(tzinfo=timezone.utc)
110
+ delta = datetime.now(timezone.utc) - last
111
+ return delta.total_seconds() / 86400.0
112
+
113
+ return 0.0
114
+
115
+ @classmethod
116
+ def build(
117
+ cls,
118
+ node: "MemoryNode",
119
+ provenance: Optional["ProvenanceRecord"] = None,
120
+ ) -> Dict[str, Any]:
121
+ """
122
+ Build a full confidence_envelope dict for the given node.
123
+
124
+ Returns a dict suitable for direct JSON serialization.
125
+ """
126
+ reliability = cls._reliability(node)
127
+ access_count: int = getattr(node, "access_count", 1)
128
+ staleness: float = cls._staleness_days(node, provenance)
129
+
130
+ # Determine source type for trust weighting
131
+ source_type = "unknown"
132
+ if provenance:
133
+ source_type = provenance.origin.type
134
+ source_trust = SOURCE_TRUST.get(source_type, 0.5)
135
+
136
+ # Contradiction check
137
+ is_contradicted = provenance.is_contradicted() if provenance else False
138
+
139
+ # Last verified date (human-readable)
140
+ last_verified: Optional[str] = None
141
+ if provenance:
142
+ for evt in reversed(provenance.lineage):
143
+ if evt.event == "verified" and evt.outcome is True:
144
+ last_verified = evt.timestamp
145
+ break
146
+
147
+ # ---- Determine level ------------------------------------ #
148
+ if is_contradicted:
149
+ level = LEVEL_CONTRADICTED
150
+ elif staleness > STALENESS_STALE_DAYS:
151
+ level = LEVEL_STALE
152
+ elif (
153
+ reliability >= RELIABILITY_HIGH_THRESHOLD
154
+ and access_count >= ACCESS_COUNT_HIGH_EVIDENCE
155
+ and source_trust >= 0.75
156
+ ):
157
+ level = LEVEL_HIGH
158
+ elif reliability >= RELIABILITY_MEDIUM_THRESHOLD and access_count >= ACCESS_COUNT_MIN_EVIDENCE:
159
+ level = LEVEL_MEDIUM
160
+ else:
161
+ level = LEVEL_LOW
162
+
163
+ envelope: Dict[str, Any] = {
164
+ "level": level,
165
+ "reliability": round(reliability, 4),
166
+ "access_count": access_count,
167
+ "staleness_days": round(staleness, 1),
168
+ "source_type": source_type,
169
+ "source_trust": round(source_trust, 2),
170
+ "is_contradicted": is_contradicted,
171
+ }
172
+ if last_verified:
173
+ envelope["last_verified"] = last_verified
174
+
175
+ return envelope
176
+
177
+
178
+ # ------------------------------------------------------------------ #
179
+ # Convenience function #
180
+ # ------------------------------------------------------------------ #
181
+
182
+ def build_confidence_envelope(
183
+ node: "MemoryNode",
184
+ provenance: Optional["ProvenanceRecord"] = None,
185
+ ) -> Dict[str, Any]:
186
+ """
187
+ Module-level shortcut for ConfidenceEnvelopeGenerator.build().
188
+
189
+ Args:
190
+ node: MemoryNode to evaluate.
191
+ provenance: Optional ProvenanceRecord for the node.
192
+
193
+ Returns:
194
+ confidence_envelope dict with level, reliability, staleness, etc.
195
+ """
196
+ return ConfidenceEnvelopeGenerator.build(node, provenance)
src/mnemocore/core/config.py CHANGED
@@ -139,6 +139,37 @@ class EncodingConfig:
139
  token_method: str = "bundle"
140
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  @dataclass(frozen=True)
143
  class DreamLoopConfig:
144
  """Configuration for the dream loop (subconscious background processing)."""
@@ -194,11 +225,19 @@ class SubconsciousAIConfig:
194
  max_memories_per_cycle: int = 10 # Process at most N memories per pulse
195
 
196
 
 
 
 
 
 
 
 
 
197
  @dataclass(frozen=True)
198
  class HAIMConfig:
199
  """Root configuration for the HAIM system."""
200
 
201
- version: str = "3.0"
202
  dimensionality: int = 16384
203
  encoding: EncodingConfig = field(default_factory=EncodingConfig)
204
  tiers_hot: TierConfig = field(
@@ -230,8 +269,13 @@ class HAIMConfig:
230
  paths: PathsConfig = field(default_factory=PathsConfig)
231
  consolidation: ConsolidationConfig = field(default_factory=ConsolidationConfig)
232
  attention_masking: AttentionMaskingConfig = field(default_factory=AttentionMaskingConfig)
 
 
 
 
233
  dream_loop: DreamLoopConfig = field(default_factory=DreamLoopConfig)
234
  subconscious_ai: SubconsciousAIConfig = field(default_factory=SubconsciousAIConfig)
 
235
 
236
 
237
  def _env_override(key: str, default):
@@ -347,15 +391,6 @@ def load_config(path: Optional[Path] = None) -> HAIMConfig:
347
  token_method=enc_raw.get("token_method", "bundle"),
348
  )
349
 
350
- # Build LTP config
351
- ltp_raw = raw.get("ltp") or {}
352
- ltp = LTPConfig(
353
- initial_importance=ltp_raw.get("initial_importance", 0.5),
354
- decay_lambda=ltp_raw.get("decay_lambda", 0.01),
355
- permanence_threshold=ltp_raw.get("permanence_threshold", 0.95),
356
- half_life_days=ltp_raw.get("half_life_days", 30.0),
357
- )
358
-
359
  # Build paths config
360
  paths_raw = raw.get("paths") or {}
361
  paths = PathsConfig(
@@ -499,6 +534,37 @@ def load_config(path: Optional[Path] = None) -> HAIMConfig:
499
  model=_env_override("DREAM_LOOP_MODEL", dream_raw.get("model", "gemma3:1b")),
500
  )
501
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
  # Build subconscious AI config (Phase 4.4 BETA)
503
  sub_raw = raw.get("subconscious_ai") or {}
504
  subconscious_ai = SubconsciousAIConfig(
@@ -524,8 +590,17 @@ def load_config(path: Optional[Path] = None) -> HAIMConfig:
524
  max_memories_per_cycle=_env_override("SUBCONSCIOUS_AI_MAX_MEMORIES_PER_CYCLE", sub_raw.get("max_memories_per_cycle", 10)),
525
  )
526
 
 
 
 
 
 
 
 
 
 
527
  return HAIMConfig(
528
- version=raw.get("version", "3.0"),
529
  dimensionality=dimensionality,
530
  encoding=encoding,
531
  tiers_hot=_build_tier("hot", hot_raw),
@@ -542,8 +617,13 @@ def load_config(path: Optional[Path] = None) -> HAIMConfig:
542
  paths=paths,
543
  consolidation=consolidation,
544
  attention_masking=attention_masking,
 
 
 
 
545
  dream_loop=dream_loop,
546
  subconscious_ai=subconscious_ai,
 
547
  )
548
 
549
 
 
139
  token_method: str = "bundle"
140
 
141
 
142
+ @dataclass(frozen=True)
143
+ class SynapseConfig:
144
+ """Configuration for Phase 12.1: Aggressive Synapse Formation"""
145
+ similarity_threshold: float = 0.5
146
+ auto_bind_on_store: bool = True
147
+ multi_hop_depth: int = 2
148
+
149
+
150
+ @dataclass(frozen=True)
151
+ class ContextConfig:
152
+ """Configuration for Phase 12.2: Contextual Awareness"""
153
+ enabled: bool = True
154
+ shift_threshold: float = 0.3
155
+ rolling_window_size: int = 5
156
+
157
+
158
+ @dataclass(frozen=True)
159
+ class PreferenceConfig:
160
+ """Configuration for Phase 12.3: Preference Learning"""
161
+ enabled: bool = True
162
+ learning_rate: float = 0.1
163
+ history_limit: int = 100
164
+
165
+
166
+ @dataclass(frozen=True)
167
+ class AnticipatoryConfig:
168
+ """Configuration for Phase 13.2: Anticipatory Memory"""
169
+ enabled: bool = True
170
+ predictive_depth: int = 1
171
+
172
+
173
  @dataclass(frozen=True)
174
  class DreamLoopConfig:
175
  """Configuration for the dream loop (subconscious background processing)."""
 
225
  max_memories_per_cycle: int = 10 # Process at most N memories per pulse
226
 
227
 
228
+ @dataclass(frozen=True)
229
+ class PulseConfig:
230
+ """Configuration for Phase 5 AGI Pulse Loop orchestrator."""
231
+ enabled: bool = True
232
+ interval_seconds: int = 30
233
+ max_agents_per_tick: int = 50
234
+ max_episodes_per_tick: int = 200
235
+
236
  @dataclass(frozen=True)
237
  class HAIMConfig:
238
  """Root configuration for the HAIM system."""
239
 
240
+ version: str = "4.5"
241
  dimensionality: int = 16384
242
  encoding: EncodingConfig = field(default_factory=EncodingConfig)
243
  tiers_hot: TierConfig = field(
 
269
  paths: PathsConfig = field(default_factory=PathsConfig)
270
  consolidation: ConsolidationConfig = field(default_factory=ConsolidationConfig)
271
  attention_masking: AttentionMaskingConfig = field(default_factory=AttentionMaskingConfig)
272
+ synapse: SynapseConfig = field(default_factory=SynapseConfig)
273
+ context: ContextConfig = field(default_factory=ContextConfig)
274
+ preference: PreferenceConfig = field(default_factory=PreferenceConfig)
275
+ anticipatory: AnticipatoryConfig = field(default_factory=AnticipatoryConfig)
276
  dream_loop: DreamLoopConfig = field(default_factory=DreamLoopConfig)
277
  subconscious_ai: SubconsciousAIConfig = field(default_factory=SubconsciousAIConfig)
278
+ pulse: PulseConfig = field(default_factory=PulseConfig)
279
 
280
 
281
  def _env_override(key: str, default):
 
391
  token_method=enc_raw.get("token_method", "bundle"),
392
  )
393
 
 
 
 
 
 
 
 
 
 
394
  # Build paths config
395
  paths_raw = raw.get("paths") or {}
396
  paths = PathsConfig(
 
534
  model=_env_override("DREAM_LOOP_MODEL", dream_raw.get("model", "gemma3:1b")),
535
  )
536
 
537
+ # Build synapse config (Phase 12.1)
538
+ syn_raw = raw.get("synapse") or {}
539
+ synapse = SynapseConfig(
540
+ similarity_threshold=_env_override("SYNAPSE_SIMILARITY_THRESHOLD", syn_raw.get("similarity_threshold", 0.5)),
541
+ auto_bind_on_store=_env_override("SYNAPSE_AUTO_BIND_ON_STORE", syn_raw.get("auto_bind_on_store", True)),
542
+ multi_hop_depth=_env_override("SYNAPSE_MULTI_HOP_DEPTH", syn_raw.get("multi_hop_depth", 2)),
543
+ )
544
+
545
+ # Build context config (Phase 12.2)
546
+ ctx_raw = raw.get("context") or {}
547
+ context = ContextConfig(
548
+ enabled=_env_override("CONTEXT_ENABLED", ctx_raw.get("enabled", True)),
549
+ shift_threshold=_env_override("CONTEXT_SHIFT_THRESHOLD", ctx_raw.get("shift_threshold", 0.3)),
550
+ rolling_window_size=_env_override("CONTEXT_ROLLING_WINDOW_SIZE", ctx_raw.get("rolling_window_size", 5)),
551
+ )
552
+
553
+ # Build preference config (Phase 12.3)
554
+ pref_raw = raw.get("preference") or {}
555
+ preference = PreferenceConfig(
556
+ enabled=_env_override("PREFERENCE_ENABLED", pref_raw.get("enabled", True)),
557
+ learning_rate=_env_override("PREFERENCE_LEARNING_RATE", pref_raw.get("learning_rate", 0.1)),
558
+ history_limit=_env_override("PREFERENCE_HISTORY_LIMIT", pref_raw.get("history_limit", 100)),
559
+ )
560
+
561
+ # Build anticipatory config (Phase 13.2)
562
+ ant_raw = raw.get("anticipatory") or {}
563
+ anticipatory = AnticipatoryConfig(
564
+ enabled=_env_override("ANTICIPATORY_ENABLED", ant_raw.get("enabled", True)),
565
+ predictive_depth=_env_override("ANTICIPATORY_PREDICTIVE_DEPTH", ant_raw.get("predictive_depth", 1)),
566
+ )
567
+
568
  # Build subconscious AI config (Phase 4.4 BETA)
569
  sub_raw = raw.get("subconscious_ai") or {}
570
  subconscious_ai = SubconsciousAIConfig(
 
590
  max_memories_per_cycle=_env_override("SUBCONSCIOUS_AI_MAX_MEMORIES_PER_CYCLE", sub_raw.get("max_memories_per_cycle", 10)),
591
  )
592
 
593
+ # Build pulse config (Phase 5.0)
594
+ pulse_raw = raw.get("pulse") or {}
595
+ pulse = PulseConfig(
596
+ enabled=_env_override("PULSE_ENABLED", pulse_raw.get("enabled", True)),
597
+ interval_seconds=_env_override("PULSE_INTERVAL_SECONDS", pulse_raw.get("interval_seconds", 30)),
598
+ max_agents_per_tick=_env_override("PULSE_MAX_AGENTS_PER_TICK", pulse_raw.get("max_agents_per_tick", 50)),
599
+ max_episodes_per_tick=_env_override("PULSE_MAX_EPISODES_PER_TICK", pulse_raw.get("max_episodes_per_tick", 200)),
600
+ )
601
+
602
  return HAIMConfig(
603
+ version=raw.get("version", "4.5"),
604
  dimensionality=dimensionality,
605
  encoding=encoding,
606
  tiers_hot=_build_tier("hot", hot_raw),
 
617
  paths=paths,
618
  consolidation=consolidation,
619
  attention_masking=attention_masking,
620
+ synapse=synapse,
621
+ context=context,
622
+ preference=preference,
623
+ anticipatory=anticipatory,
624
  dream_loop=dream_loop,
625
  subconscious_ai=subconscious_ai,
626
+ pulse=pulse,
627
  )
628
 
629
 
src/mnemocore/core/container.py CHANGED
@@ -12,6 +12,14 @@ from .config import HAIMConfig
12
  from .async_storage import AsyncRedisStorage
13
  from .qdrant_store import QdrantStore
14
 
 
 
 
 
 
 
 
 
15
 
16
  @dataclass
17
  class Container:
@@ -21,6 +29,14 @@ class Container:
21
  config: HAIMConfig
22
  redis_storage: Optional[AsyncRedisStorage] = None
23
  qdrant_store: Optional[QdrantStore] = None
 
 
 
 
 
 
 
 
24
 
25
 
26
  def build_container(config: HAIMConfig) -> Container:
@@ -57,6 +73,14 @@ def build_container(config: HAIMConfig) -> Container:
57
  hnsw_ef_construct=config.qdrant.hnsw_ef_construct,
58
  )
59
 
 
 
 
 
 
 
 
 
60
  return container
61
 
62
 
 
12
  from .async_storage import AsyncRedisStorage
13
  from .qdrant_store import QdrantStore
14
 
15
+ # Phase 5 AGI Services
16
+ from .working_memory import WorkingMemoryService
17
+ from .episodic_store import EpisodicStoreService
18
+ from .semantic_store import SemanticStoreService
19
+ from .procedural_store import ProceduralStoreService
20
+ from .meta_memory import MetaMemoryService
21
+ from .agent_profile import AgentProfileService
22
+
23
 
24
  @dataclass
25
  class Container:
 
29
  config: HAIMConfig
30
  redis_storage: Optional[AsyncRedisStorage] = None
31
  qdrant_store: Optional[QdrantStore] = None
32
+
33
+ # Phase 5 Services
34
+ working_memory: Optional[WorkingMemoryService] = None
35
+ episodic_store: Optional[EpisodicStoreService] = None
36
+ semantic_store: Optional[SemanticStoreService] = None
37
+ procedural_store: Optional[ProceduralStoreService] = None
38
+ meta_memory: Optional[MetaMemoryService] = None
39
+ agent_profiles: Optional[AgentProfileService] = None
40
 
41
 
42
  def build_container(config: HAIMConfig) -> Container:
 
73
  hnsw_ef_construct=config.qdrant.hnsw_ef_construct,
74
  )
75
 
76
+ # Initialize Phase 5 AGI Services
77
+ container.working_memory = WorkingMemoryService()
78
+ container.episodic_store = EpisodicStoreService()
79
+ container.semantic_store = SemanticStoreService(qdrant_store=container.qdrant_store)
80
+ container.procedural_store = ProceduralStoreService()
81
+ container.meta_memory = MetaMemoryService()
82
+ container.agent_profiles = AgentProfileService()
83
+
84
  return container
85
 
86
 
src/mnemocore/core/contradiction.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Contradiction Detection Module (Phase 5.0)
3
+ ==========================================
4
+ Detects contradicting memories in MnemoCore using a two-stage pipeline:
5
+
6
+ Stage 1: TextEncoder similarity search (fast, vector-based)
7
+ - At /store time: compare new memory against top-5 existing memories
8
+ - If similarity > SIMILARITY_THRESHOLD (0.80) → proceed to Stage 2
9
+
10
+ Stage 2: LLM-based semantic comparison (accurate, but heavier)
11
+ - Uses SubconsciousAI connector to evaluate if two memories actually contradict
12
+ - Avoids false positives from paraphrases (similarity doesn't mean contradiction)
13
+
14
+ On confirmed contradiction:
15
+ - Both memories receive a 'contradiction_group_id' in their provenance lineage
16
+ - Both are flagged in their metadata
17
+ - The API returns an alert in the store response
18
+ - Entries are added to a ContradictionRegistry for the /contradictions endpoint
19
+
20
+ Background scan:
21
+ - ContradictionDetector.scan(nodes) can be called from ConsolidationWorker
22
+
23
+ Public API:
24
+ detector = ContradictionDetector(engine)
25
+ result = await detector.check_on_store(new_content, new_node, existing_nodes)
26
+ all = detector.registry.list_all()
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import uuid
32
+ from dataclasses import dataclass, field
33
+ from datetime import datetime, timezone
34
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
35
+
36
+ from loguru import logger
37
+
38
+ if TYPE_CHECKING:
39
+ from .node import MemoryNode
40
+
41
+
42
+ # ------------------------------------------------------------------ #
43
+ # Thresholds #
44
+ # ------------------------------------------------------------------ #
45
+
46
+ SIMILARITY_THRESHOLD: float = 0.80 # Above this → suspect contradiction
47
+ LLM_CONFIRM_MIN_SCORE: float = 0.70 # LLM contradiction confidence minimum
48
+
49
+
50
+ # ------------------------------------------------------------------ #
51
+ # ContradictionRecord #
52
+ # ------------------------------------------------------------------ #
53
+
54
+ @dataclass
55
+ class ContradictionRecord:
56
+ """A detected contradiction between two memories."""
57
+ group_id: str = field(default_factory=lambda: f"cg_{uuid.uuid4().hex[:12]}")
58
+ memory_a_id: str = ""
59
+ memory_b_id: str = ""
60
+ similarity_score: float = 0.0
61
+ llm_confirmed: bool = False
62
+ detected_at: str = field(
63
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
64
+ )
65
+ resolved: bool = False
66
+ resolution_note: Optional[str] = None
67
+
68
+ def to_dict(self) -> Dict[str, Any]:
69
+ return {
70
+ "group_id": self.group_id,
71
+ "memory_a_id": self.memory_a_id,
72
+ "memory_b_id": self.memory_b_id,
73
+ "similarity_score": round(self.similarity_score, 4),
74
+ "llm_confirmed": self.llm_confirmed,
75
+ "detected_at": self.detected_at,
76
+ "resolved": self.resolved,
77
+ "resolution_note": self.resolution_note,
78
+ }
79
+
80
+
81
+ # ------------------------------------------------------------------ #
82
+ # ContradictionRegistry #
83
+ # ------------------------------------------------------------------ #
84
+
85
+ class ContradictionRegistry:
86
+ """In-memory store of detected contradictions (survives until restart)."""
87
+
88
+ def __init__(self) -> None:
89
+ self._records: Dict[str, ContradictionRecord] = {}
90
+
91
+ def register(self, record: ContradictionRecord) -> None:
92
+ self._records[record.group_id] = record
93
+
94
+ def resolve(self, group_id: str, note: Optional[str] = None) -> bool:
95
+ if group_id in self._records:
96
+ self._records[group_id].resolved = True
97
+ self._records[group_id].resolution_note = note
98
+ return True
99
+ return False
100
+
101
+ def list_all(self, unresolved_only: bool = True) -> List[ContradictionRecord]:
102
+ recs = list(self._records.values())
103
+ if unresolved_only:
104
+ recs = [r for r in recs if not r.resolved]
105
+ return sorted(recs, key=lambda r: r.detected_at, reverse=True)
106
+
107
+ def list_for_memory(self, memory_id: str) -> List[ContradictionRecord]:
108
+ return [
109
+ r for r in self._records.values()
110
+ if r.memory_a_id == memory_id or r.memory_b_id == memory_id
111
+ ]
112
+
113
+ def __len__(self) -> int:
114
+ return len([r for r in self._records.values() if not r.resolved])
115
+
116
+
117
+ # ------------------------------------------------------------------ #
118
+ # ContradictionDetector #
119
+ # ------------------------------------------------------------------ #
120
+
121
+ class ContradictionDetector:
122
+ """
123
+ Two-stage contradiction detector.
124
+
125
+ Stage 1: Vector similarity via the engine's binary HDV comparison.
126
+ Stage 2: LLM semantic check via SubconsciousAI (optional).
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ engine=None, # HAIMEngine — optional; if None, similarity check uses fallback
132
+ similarity_threshold: float = SIMILARITY_THRESHOLD,
133
+ top_k: int = 5,
134
+ use_llm: bool = True,
135
+ ) -> None:
136
+ self.engine = engine
137
+ self.similarity_threshold = similarity_threshold
138
+ self.top_k = top_k
139
+ self.use_llm = use_llm
140
+ self.registry = ContradictionRegistry()
141
+
142
+ # ---- Similarity helpers -------------------------------------- #
143
+
144
+ def _hamming_similarity(self, node_a: "MemoryNode", node_b: "MemoryNode") -> float:
145
+ """
146
+ Compute binary HDV similarity between two nodes.
147
+ Similarity = 1 - normalized_hamming_distance.
148
+ """
149
+ try:
150
+ import numpy as np
151
+ a = node_a.hdv.data
152
+ b = node_b.hdv.data
153
+ xor = np.bitwise_xor(a, b)
154
+ ham = float(bin(int.from_bytes(xor.tobytes(), "little")).count("1"))
155
+ dim = len(a) * 8
156
+ return 1.0 - ham / dim
157
+ except Exception:
158
+ return 0.0
159
+
160
+ # ---- LLM contradiction check --------------------------------- #
161
+
162
+ async def _llm_contradicts(
163
+ self, content_a: str, content_b: str
164
+ ) -> Tuple[bool, float]:
165
+ """
166
+ Ask SubconsciousAI if two contents contradict each other.
167
+ Returns (is_contradiction, confidence_score).
168
+ Falls back to False if LLM is unavailable.
169
+ """
170
+ if not self.engine or not self.use_llm:
171
+ return False, 0.0
172
+
173
+ try:
174
+ subcon = getattr(self.engine, "subconscious_ai", None)
175
+ if subcon is None:
176
+ return False, 0.0
177
+
178
+ prompt = (
179
+ "Do the following two statements contradict each other? "
180
+ "Answer with a JSON object: {\"contradiction\": true/false, \"confidence\": 0.0-1.0}.\n\n"
181
+ f"Statement A: {content_a[:500]}\n"
182
+ f"Statement B: {content_b[:500]}"
183
+ )
184
+ raw = await subcon.generate(prompt, max_tokens=64)
185
+ import json as _json
186
+ parsed = _json.loads(raw.strip())
187
+ return bool(parsed.get("contradiction", False)), float(parsed.get("confidence", 0.0))
188
+ except Exception as exc:
189
+ logger.debug(f"LLM contradiction check failed: {exc}")
190
+ return False, 0.0
191
+
192
+ # ---- Flag helpers ------------------------------------------- #
193
+
194
+ def _flag_node(self, node: "MemoryNode", group_id: str) -> None:
195
+ """Attach contradiction metadata to a node's provenance and metadata fields."""
196
+ node.metadata["contradiction_group_id"] = group_id
197
+ node.metadata["contradicted_at"] = datetime.now(timezone.utc).isoformat()
198
+
199
+ prov = getattr(node, "provenance", None)
200
+ if prov is not None:
201
+ prov.mark_contradicted(group_id)
202
+
203
+ # ---- Main API ------------------------------------------------ #
204
+
205
+ async def check_on_store(
206
+ self,
207
+ new_node: "MemoryNode",
208
+ candidates: Optional[List["MemoryNode"]] = None,
209
+ ) -> Optional[ContradictionRecord]:
210
+ """
211
+ Check a newly stored node against existing memories.
212
+
213
+ Args:
214
+ new_node: The node just stored.
215
+ candidates: Optional pre-fetched list of nodes to compare against.
216
+ If None and engine is available, fetches via HDV search.
217
+
218
+ Returns:
219
+ ContradictionRecord if a contradiction was detected, else None.
220
+ """
221
+ # Fetch candidates if not provided
222
+ if candidates is None and self.engine is not None:
223
+ try:
224
+ results = await self.engine.query(
225
+ new_node.content, top_k=self.top_k
226
+ )
227
+ nodes = []
228
+ for mem_id, _score in results:
229
+ n = await self.engine.get_memory(mem_id)
230
+ if n and n.id != new_node.id:
231
+ nodes.append(n)
232
+ candidates = nodes
233
+ except Exception as e:
234
+ logger.debug(f"ContradictionDetector: candidate fetch failed: {e}")
235
+ candidates = []
236
+
237
+ if not candidates:
238
+ return None
239
+
240
+ # Stage 1: similarity filter
241
+ high_sim_candidates = []
242
+ for cand in candidates:
243
+ sim = self._hamming_similarity(new_node, cand)
244
+ if sim >= self.similarity_threshold:
245
+ high_sim_candidates.append((cand, sim))
246
+
247
+ if not high_sim_candidates:
248
+ return None
249
+
250
+ # Stage 2: LLM confirmation for the highest-similarity candidate
251
+ high_sim_candidates.sort(key=lambda x: x[1], reverse=True)
252
+ top_cand, top_sim = high_sim_candidates[0]
253
+
254
+ is_contradiction = False
255
+ llm_confirmed = False
256
+
257
+ if self.use_llm:
258
+ is_contradiction, conf = await self._llm_contradicts(
259
+ new_node.content, top_cand.content
260
+ )
261
+ llm_confirmed = is_contradiction and conf >= LLM_CONFIRM_MIN_SCORE
262
+ else:
263
+ # Without LLM, use high similarity as a soft contradiction signal
264
+ is_contradiction = top_sim >= 0.90
265
+ llm_confirmed = False
266
+
267
+ if not is_contradiction:
268
+ return None
269
+
270
+ # Register the contradiction
271
+ record = ContradictionRecord(
272
+ memory_a_id=new_node.id,
273
+ memory_b_id=top_cand.id,
274
+ similarity_score=top_sim,
275
+ llm_confirmed=llm_confirmed,
276
+ )
277
+ self.registry.register(record)
278
+ self._flag_node(new_node, record.group_id)
279
+ self._flag_node(top_cand, record.group_id)
280
+
281
+ logger.warning(
282
+ f"⚠️ Contradiction detected: {new_node.id[:8]} ↔ {top_cand.id[:8]} "
283
+ f"(sim={top_sim:.3f}, llm_confirmed={llm_confirmed}, group={record.group_id})"
284
+ )
285
+ return record
286
+
287
+ async def scan(self, nodes: "List[MemoryNode]") -> List[ContradictionRecord]:
288
+ """
289
+ Background scan: compare each node against its peers in the provided list.
290
+ Called periodically from ConsolidationWorker.
291
+
292
+ Returns all newly detected contradiction records.
293
+ """
294
+ found: List[ContradictionRecord] = []
295
+ n = len(nodes)
296
+ for i in range(n):
297
+ for j in range(i + 1, n):
298
+ sim = self._hamming_similarity(nodes[i], nodes[j])
299
+ if sim < self.similarity_threshold:
300
+ continue
301
+ is_contradiction, _ = await self._llm_contradicts(
302
+ nodes[i].content, nodes[j].content
303
+ )
304
+ if not is_contradiction:
305
+ continue
306
+ record = ContradictionRecord(
307
+ memory_a_id=nodes[i].id,
308
+ memory_b_id=nodes[j].id,
309
+ similarity_score=sim,
310
+ llm_confirmed=True,
311
+ )
312
+ self.registry.register(record)
313
+ self._flag_node(nodes[i], record.group_id)
314
+ self._flag_node(nodes[j], record.group_id)
315
+ found.append(record)
316
+
317
+ if found:
318
+ logger.info(f"ContradictionDetector background scan: {len(found)} contradictions found in {n} nodes")
319
+ return found
320
+
321
+
322
+ # ------------------------------------------------------------------ #
323
+ # Module singleton #
324
+ # ------------------------------------------------------------------ #
325
+
326
+ _DETECTOR: ContradictionDetector | None = None
327
+
328
+
329
+ def get_contradiction_detector(engine=None) -> ContradictionDetector:
330
+ """Return the shared ContradictionDetector singleton."""
331
+ global _DETECTOR
332
+ if _DETECTOR is None:
333
+ _DETECTOR = ContradictionDetector(engine=engine)
334
+ elif engine is not None and _DETECTOR.engine is None:
335
+ _DETECTOR.engine = engine
336
+ return _DETECTOR
src/mnemocore/core/cross_domain.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cross-Domain Association Builder (Phase 5.0 — Agent 3)
3
+ =======================================================
4
+ Automatically links memories across three semantic domains:
5
+
6
+ strategic – goals, decisions, roadmaps, strategies
7
+ operational – code, bugs, documentation, tasks
8
+ personal – preferences, habits, relationships, context
9
+
10
+ Cross-domain synapses improve holistic reasoning: when a strategic
11
+ goal changes, the system can surface related operational tasks or
12
+ personal context without being explicitly queried.
13
+
14
+ Implementation:
15
+ - Each memory is tagged with a `domain` in its metadata (or inferred)
16
+ - CrossDomainSynapseBuilder monitors recently stored memories
17
+ - Co-occurrence within a time window → create a cross-domain synapse
18
+ - Synapse weight is damped (0.2×) relative to intra-domain (1.0×)
19
+
20
+ Integration with RippleContext:
21
+ - ripple_context.py uses domain_weight when propagating context
22
+ - Cross-domain propagation uses CROSS_DOMAIN_WEIGHT as the multiplier
23
+
24
+ Public API:
25
+ builder = CrossDomainSynapseBuilder(engine)
26
+ await builder.process_new_memory(node) # call after /store
27
+ pairs = await builder.scan_recent(hours=1) # background scan
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from datetime import datetime, timezone, timedelta
33
+ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
34
+ from loguru import logger
35
+
36
+ if TYPE_CHECKING:
37
+ from .node import MemoryNode
38
+
39
+
40
+ # ------------------------------------------------------------------ #
41
+ # Constants #
42
+ # ------------------------------------------------------------------ #
43
+
44
+ DOMAINS = {"strategic", "operational", "personal"}
45
+ DEFAULT_DOMAIN = "operational"
46
+
47
+ # Weight applied to cross-domain synapses (vs 1.0 for intra-domain)
48
+ CROSS_DOMAIN_WEIGHT: float = 0.2
49
+
50
+ # Time window for co-occurrence detection (hours)
51
+ COOCCURRENCE_WINDOW_HOURS: float = 2.0
52
+
53
+ # Keywords used to infer domain automatically if not tagged
54
+ DOMAIN_KEYWORDS: Dict[str, List[str]] = {
55
+ "strategic": [
56
+ "goal", "strategy", "roadmap", "vision", "mission", "objective",
57
+ "decision", "priority", "kpi", "okr", "plan", "budget", "market",
58
+ ],
59
+ "personal": [
60
+ "prefer", "habit", "feel", "emotion", "prefer", "like", "dislike",
61
+ "relationship", "trust", "colleague", "friend", "name", "remember me",
62
+ ],
63
+ "operational": [
64
+ "code", "bug", "fix", "implement", "test", "deploy", "api",
65
+ "function", "class", "module", "error", "exception", "task", "ticket",
66
+ ],
67
+ }
68
+
69
+
70
+ # ------------------------------------------------------------------ #
71
+ # Domain inference #
72
+ # ------------------------------------------------------------------ #
73
+
74
+ def infer_domain(content: str, metadata: Optional[Dict] = None) -> str:
75
+ """
76
+ Infer the semantic domain of a memory from its content and metadata.
77
+
78
+ Priority:
79
+ 1. metadata["domain"] if set
80
+ 2. keyword match in content (highest score wins)
81
+ 3. DEFAULT_DOMAIN ("operational")
82
+ """
83
+ if metadata and "domain" in metadata:
84
+ d = metadata["domain"].lower()
85
+ return d if d in DOMAINS else DEFAULT_DOMAIN
86
+
87
+ content_lower = content.lower()
88
+ best_domain = DEFAULT_DOMAIN
89
+ best_count = 0
90
+
91
+ for domain, keywords in DOMAIN_KEYWORDS.items():
92
+ count = sum(1 for kw in keywords if kw in content_lower)
93
+ if count > best_count:
94
+ best_count = count
95
+ best_domain = domain
96
+
97
+ return best_domain
98
+
99
+
100
+ # ------------------------------------------------------------------ #
101
+ # CrossDomainSynapseBuilder #
102
+ # ------------------------------------------------------------------ #
103
+
104
+ class CrossDomainSynapseBuilder:
105
+ """
106
+ Detects cross-domain co-occurrences and requests synapse creation.
107
+
108
+ Works by maintaining a rolling buffer of recently stored memories,
109
+ then pairing memories from different domains that appeared within
110
+ COOCCURRENCE_WINDOW_HOURS of each other.
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ engine=None, # HAIMEngine
116
+ window_hours: float = COOCCURRENCE_WINDOW_HOURS,
117
+ cross_domain_weight: float = CROSS_DOMAIN_WEIGHT,
118
+ ) -> None:
119
+ self.engine = engine
120
+ self.window = timedelta(hours=window_hours)
121
+ self.weight = cross_domain_weight
122
+ # Buffer: list of (node_id, domain, stored_at)
123
+ self._buffer: List[Tuple[str, str, datetime]] = []
124
+
125
+ # ---- Domain helpers ------------------------------------------ #
126
+
127
+ def tag_domain(self, node: "MemoryNode") -> str:
128
+ """Infer and write domain tag to node.metadata. Returns domain string."""
129
+ domain = infer_domain(node.content, getattr(node, "metadata", {}))
130
+ if hasattr(node, "metadata"):
131
+ node.metadata["domain"] = domain
132
+ return domain
133
+
134
+ # ---- Synapse creation --------------------------------------- #
135
+
136
+ async def _create_synapse(self, id_a: str, id_b: str) -> None:
137
+ """
138
+ Request synapse creation between two nodes via the engine's synapse index.
139
+ Weight is damped by CROSS_DOMAIN_WEIGHT.
140
+ """
141
+ if self.engine is None:
142
+ logger.debug(f"CrossDomain: no engine, skipping synapse {id_a[:8]} ↔ {id_b[:8]}")
143
+ return
144
+ try:
145
+ synapse_index = getattr(self.engine, "synapse_index", None)
146
+ if synapse_index is not None:
147
+ synapse_index.add_or_strengthen(id_a, id_b, delta=self.weight)
148
+ logger.debug(
149
+ f"CrossDomain synapse created: {id_a[:8]} ↔ {id_b[:8]} weight={self.weight}"
150
+ )
151
+ except Exception as exc:
152
+ logger.debug(f"CrossDomain synapse creation failed: {exc}")
153
+
154
+ # ---- Main API ----------------------------------------------- #
155
+
156
+ async def process_new_memory(self, node: "MemoryNode") -> List[Tuple[str, str]]:
157
+ """
158
+ Called after a new memory is stored.
159
+ Tags its domain and checks for cross-domain co-occurrences in the buffer.
160
+
161
+ Returns list of (id_a, id_b) pairs for which synapses were created.
162
+ """
163
+ domain = self.tag_domain(node)
164
+ now = datetime.now(timezone.utc)
165
+
166
+ # Cut stale entries from buffer
167
+ cutoff = now - self.window
168
+ self._buffer = [(nid, d, ts) for nid, d, ts in self._buffer if ts >= cutoff]
169
+
170
+ # Find cross-domain pairs with current node
171
+ pairs: List[Tuple[str, str]] = []
172
+ already_seen: Set[str] = set()
173
+ for existing_id, existing_domain, _ts in self._buffer:
174
+ if existing_domain != domain and existing_id not in already_seen:
175
+ await self._create_synapse(node.id, existing_id)
176
+ pairs.append((node.id, existing_id))
177
+ already_seen.add(existing_id)
178
+
179
+ # Add current node to buffer
180
+ self._buffer.append((node.id, domain, now))
181
+
182
+ if pairs:
183
+ logger.info(
184
+ f"CrossDomain: {len(pairs)} cross-domain synapses created for node {node.id[:8]} (domain={domain})"
185
+ )
186
+ return pairs
187
+
188
+ async def scan_recent(self, hours: float = 1.0) -> List[Tuple[str, str]]:
189
+ """
190
+ Scan the current buffer for any unpaired cross-domain co-occurrences.
191
+ Returns all cross-domain pairs.
192
+ """
193
+ now = datetime.now(timezone.utc)
194
+ cutoff = now - timedelta(hours=hours)
195
+ recent = [(nid, d, ts) for nid, d, ts in self._buffer if ts >= cutoff]
196
+
197
+ pairs: List[Tuple[str, str]] = []
198
+ n = len(recent)
199
+ for i in range(n):
200
+ for j in range(i + 1, n):
201
+ id_i, dom_i, _ = recent[i]
202
+ id_j, dom_j, _ = recent[j]
203
+ if dom_i != dom_j:
204
+ await self._create_synapse(id_i, id_j)
205
+ pairs.append((id_i, id_j))
206
+
207
+ return pairs
208
+
209
+ def clear_buffer(self) -> None:
210
+ """Reset the co-occurrence buffer."""
211
+ self._buffer.clear()
src/mnemocore/core/emotional_tag.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Emotional Tagging Module (Phase 5.0 — Agent 3)
3
+ ================================================
4
+ Adds valence/arousal emotional metadata to MemoryNode storage.
5
+
6
+ Based on affective computing research (Russell's circumplex model):
7
+ - emotional_valence: float in [-1.0, 1.0]
8
+ -1.0 = extremely negative (fear, grief)
9
+ 0.0 = neutral
10
+ +1.0 = extremely positive (joy, excitement)
11
+
12
+ - emotional_arousal: float in [0.0, 1.0]
13
+ 0.0 = calm / low energy
14
+ 1.0 = highly activated / intense
15
+
16
+ These signals are used by the SubconsciousAI dream cycle to prioritize
17
+ consolidation of high-valence, high-arousal memories (the most
18
+ biologically significant ones).
19
+
20
+ Public API:
21
+ tag = EmotionalTag(valence=0.8, arousal=0.9)
22
+ meta = tag.to_metadata_dict()
23
+ tag_back = EmotionalTag.from_metadata(node.metadata)
24
+ score = tag.salience() # combined importance weight
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from dataclasses import dataclass
30
+ from typing import Any, Dict, Optional, TYPE_CHECKING
31
+
32
+ if TYPE_CHECKING:
33
+ from .node import MemoryNode
34
+
35
+
36
+ # ------------------------------------------------------------------ #
37
+ # EmotionalTag #
38
+ # ------------------------------------------------------------------ #
39
+
40
+ @dataclass
41
+ class EmotionalTag:
42
+ """
43
+ Two-dimensional emotional metadata for a memory.
44
+
45
+ valence ∈ [-1.0, 1.0] (-1 = very negative, +1 = very positive)
46
+ arousal ∈ [ 0.0, 1.0] ( 0 = calm, 1 = highly activated)
47
+ """
48
+
49
+ valence: float = 0.0
50
+ arousal: float = 0.0
51
+
52
+ def __post_init__(self) -> None:
53
+ self.valence = float(max(-1.0, min(1.0, self.valence)))
54
+ self.arousal = float(max(0.0, min(1.0, self.arousal)))
55
+
56
+ # ---- Salience ------------------------------------------------ #
57
+
58
+ def salience(self) -> float:
59
+ """
60
+ Combined salience score for dream cycle prioritization.
61
+ High |valence| AND high arousal = most memorable / worth consolidating.
62
+
63
+ Returns a float in [0.0, 1.0].
64
+ """
65
+ return abs(self.valence) * self.arousal
66
+
67
+ def is_emotionally_significant(self, threshold: float = 0.3) -> bool:
68
+ """True if the salience is above the given threshold."""
69
+ return self.salience() >= threshold
70
+
71
+ # ---- Serialization ------------------------------------------- #
72
+
73
+ def to_metadata_dict(self) -> Dict[str, Any]:
74
+ return {
75
+ "emotional_valence": self.valence,
76
+ "emotional_arousal": self.arousal,
77
+ "emotional_salience": round(self.salience(), 4),
78
+ }
79
+
80
+ @classmethod
81
+ def from_metadata(cls, metadata: Dict[str, Any]) -> "EmotionalTag":
82
+ """Extract an EmotionalTag from a MemoryNode's metadata dict."""
83
+ return cls(
84
+ valence=float(metadata.get("emotional_valence", 0.0)),
85
+ arousal=float(metadata.get("emotional_arousal", 0.0)),
86
+ )
87
+
88
+ @classmethod
89
+ def from_node(cls, node: "MemoryNode") -> "EmotionalTag":
90
+ """Extract emotional tag directly from a MemoryNode."""
91
+ return cls.from_metadata(getattr(node, "metadata", {}))
92
+
93
+ # ---- Helpers -------------------------------------------------- #
94
+
95
+ @classmethod
96
+ def neutral(cls) -> "EmotionalTag":
97
+ return cls(valence=0.0, arousal=0.0)
98
+
99
+ @classmethod
100
+ def high_positive(cls) -> "EmotionalTag":
101
+ """Factory for highly positive, highly aroused tags (e.g. breakthrough)."""
102
+ return cls(valence=1.0, arousal=1.0)
103
+
104
+ @classmethod
105
+ def high_negative(cls) -> "EmotionalTag":
106
+ """Factory for highly negative, highly aroused tags (e.g. critical failure)."""
107
+ return cls(valence=-1.0, arousal=1.0)
108
+
109
+ def __repr__(self) -> str:
110
+ return f"EmotionalTag(valence={self.valence:+.2f}, arousal={self.arousal:.2f}, salience={self.salience():.2f})"
111
+
112
+
113
+ # ------------------------------------------------------------------ #
114
+ # Node helpers #
115
+ # ------------------------------------------------------------------ #
116
+
117
+ def attach_emotional_tag(node: "MemoryNode", tag: EmotionalTag) -> None:
118
+ """Write emotional metadata into node.metadata in place."""
119
+ node.metadata.update(tag.to_metadata_dict())
120
+
121
+
122
+ def get_emotional_tag(node: "MemoryNode") -> EmotionalTag:
123
+ """Read the emotional tag from a node's metadata (returns neutral if absent)."""
124
+ return EmotionalTag.from_node(node)
src/mnemocore/core/engine.py CHANGED
@@ -39,6 +39,11 @@ from .gap_filler import GapFiller, GapFillerConfig
39
  from .synapse_index import SynapseIndex
40
  from .subconscious_ai import SubconsciousAIWorker
41
 
 
 
 
 
 
42
  # Phase 4.5: Recursive Synthesis Engine
43
  from .recursive_synthesizer import RecursiveSynthesizer, SynthesizerConfig
44
 
@@ -71,6 +76,9 @@ class HAIMEngine:
71
  persist_path: Optional[str] = None,
72
  config: Optional[HAIMConfig] = None,
73
  tier_manager: Optional[TierManager] = None,
 
 
 
74
  ):
75
  """
76
  Initialize HAIMEngine with optional dependency injection.
@@ -80,6 +88,9 @@ class HAIMEngine:
80
  persist_path: Path to memory persistence file.
81
  config: Configuration object. If None, uses global get_config().
82
  tier_manager: TierManager instance. If None, creates a new one.
 
 
 
83
  """
84
  self.config = config or get_config()
85
  self.dimension = self.config.dimensionality
@@ -89,6 +100,11 @@ class HAIMEngine:
89
 
90
  # Core Components
91
  self.tier_manager = tier_manager or TierManager(config=self.config)
 
 
 
 
 
92
  self.binary_encoder = TextEncoder(self.dimension)
93
 
94
  # ── Phase 3.x: synapse raw dicts (kept for backward compat) ──
@@ -132,6 +148,23 @@ class HAIMEngine:
132
 
133
  # ── Phase 4.5: recursive synthesizer ───────────────────────────
134
  self._recursive_synthesizer: Optional[RecursiveSynthesizer] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  # Conceptual Layer (VSA Soul)
137
  data_dir = self.config.paths.data_dir
@@ -337,6 +370,24 @@ class HAIMEngine:
337
  """
338
  _is_gap_fill = metadata.get("source") == "llm_gap_fill"
339
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  self.subconscious_queue.append(node.id)
341
 
342
  if not _is_gap_fill:
@@ -346,6 +397,9 @@ class HAIMEngine:
346
  # Main store() method - Orchestration only
347
  # ==========================================================================
348
 
 
 
 
349
  @timer(STORE_DURATION_SECONDS, labels={"tier": "hot"})
350
  @traced("store_memory")
351
  async def store(
@@ -359,20 +413,38 @@ class HAIMEngine:
359
  Store new memory with holographic encoding.
360
 
361
  This method orchestrates the memory storage pipeline:
362
- 1. Encode input content
363
- 2. Evaluate tier placement via EIG
364
- 3. Persist to storage
365
- 4. Trigger post-store processing
 
366
 
367
  Args:
368
- content: The text content to store.
369
  metadata: Optional metadata dictionary.
370
  goal_id: Optional goal identifier for context binding.
371
  project_id: Optional project identifier for isolation masking (Phase 4.1).
372
 
373
  Returns:
374
  The unique identifier of the stored memory node.
 
 
 
 
375
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  # 1. Encode input and bind goal context
377
  encoded_vec, updated_metadata = await self._encode_input(content, metadata, goal_id)
378
 
@@ -387,6 +459,35 @@ class HAIMEngine:
387
  # 3. Create and persist memory node
388
  node = await self._persist_memory(content, encoded_vec, updated_metadata)
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  # 4. Trigger post-store processing
391
  await self._trigger_post_store(node, updated_metadata)
392
 
@@ -412,24 +513,24 @@ class HAIMEngine:
412
  if node_id in self.subconscious_queue:
413
  self.subconscious_queue.remove(node_id)
414
 
415
- # 3. Phase 4.0: clean up via SynapseIndex (O(k))
 
416
  async with self.synapse_lock:
417
  removed_count = self._synapse_index.remove_node(node_id)
418
 
419
- # Rebuild legacy dicts
420
- self.synapses = dict(self._synapse_index.items())
421
- self.synapse_adjacency = {}
422
- for syn in self._synapse_index.values():
423
- self.synapse_adjacency.setdefault(syn.neuron_a_id, [])
424
- self.synapse_adjacency.setdefault(syn.neuron_b_id, [])
425
- self.synapse_adjacency[syn.neuron_a_id].append(syn)
426
- self.synapse_adjacency[syn.neuron_b_id].append(syn)
427
-
428
  if removed_count:
429
  await self._save_synapses()
430
 
431
  return deleted
432
 
 
 
 
 
 
 
 
 
433
  async def close(self):
434
  """Perform graceful shutdown of engine components."""
435
  logger.info("Shutting down HAIMEngine...")
@@ -461,6 +562,8 @@ class HAIMEngine:
461
  chrono_weight: bool = True,
462
  chrono_lambda: float = 0.0001,
463
  include_neighbors: bool = False,
 
 
464
  ) -> List[Tuple[str, float]]:
465
  """
466
  Query memories using Hamming distance.
@@ -480,10 +583,18 @@ class HAIMEngine:
480
  Formula: Final_Score = Semantic_Similarity * (1 / (1 + lambda * Time_Delta))
481
  - chrono_lambda: Decay rate in seconds^-1 (default: 0.0001 ~ 2.7h half-life).
482
  - include_neighbors: Also fetch temporal neighbors (previous/next) for top results.
 
 
 
483
  """
484
  # Encode Query
485
  query_vec = await self._run_in_thread(self.binary_encoder.encode, query_text)
486
 
 
 
 
 
 
487
  # Phase 4.1: Apply project isolation mask to query
488
  if project_id:
489
  query_vec = self.isolation_masker.apply_mask(query_vec, project_id)
@@ -494,6 +605,8 @@ class HAIMEngine:
494
  query_vec,
495
  top_k=top_k * 2,
496
  time_range=time_range,
 
 
497
  )
498
 
499
  scores: Dict[str, float] = {}
@@ -515,13 +628,38 @@ class HAIMEngine:
515
  if chrono_weight and score > 0:
516
  mem = mem_map.get(nid)
517
  if mem:
518
- time_delta = now_ts - mem.created_at.timestamp() # seconds since creation
519
  # Formula: Final = Semantic * (1 / (1 + lambda * time_delta))
520
  decay_factor = 1.0 / (1.0 + chrono_lambda * time_delta)
521
  score = score * decay_factor
522
 
 
 
 
 
 
 
 
 
 
 
523
  scores[nid] = score
524
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  # 2. Associative Spreading (via SynapseIndex for O(1) adjacency lookup)
526
  if associative_jump and self._synapse_index:
527
  top_seeds = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:3]
@@ -540,6 +678,15 @@ class HAIMEngine:
540
  if neighbor not in augmented_scores:
541
  mem = await self.tier_manager.get_memory(neighbor)
542
  if mem:
 
 
 
 
 
 
 
 
 
543
  augmented_scores[neighbor] = query_vec.similarity(mem.hdv)
544
 
545
  if neighbor in augmented_scores:
@@ -595,6 +742,15 @@ class HAIMEngine:
595
  if mem.previous_id:
596
  prev_mem = await self.tier_manager.get_memory(mem.previous_id)
597
  if prev_mem and prev_mem.id not in scores:
 
 
 
 
 
 
 
 
 
598
  neighbor_ids.add(prev_mem.id)
599
 
600
  # Try to find the memory that follows this one (has this as previous_id).
@@ -614,8 +770,35 @@ class HAIMEngine:
614
  # Re-sort after adding neighbors, but preserve query() top_k contract.
615
  top_results = sorted(top_results, key=lambda x: x[1], reverse=True)[:top_k]
616
 
 
 
 
 
 
 
 
617
  return top_results
618
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
619
  async def _background_dream(self, depth: int = 2):
620
  """
621
  Passive Subconscious – strengthen synapses in idle cycles.
@@ -668,12 +851,32 @@ class HAIMEngine:
668
 
669
  return sorted(active_nodes, key=score, reverse=True)[:max_collapse]
670
 
671
- async def bind_memories(self, id_a: str, id_b: str, success: bool = True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
  """
673
  Bind two memories by ID.
674
 
675
- Phase 4.0: delegates to SynapseIndex for O(1) insert/fire.
676
- Also syncs legacy dicts for backward-compat.
 
677
  """
678
  mem_a = await self.tier_manager.get_memory(id_a)
679
  mem_b = await self.tier_manager.get_memory(id_b)
@@ -682,17 +885,7 @@ class HAIMEngine:
682
  return
683
 
684
  async with self.synapse_lock:
685
- syn = self._synapse_index.add_or_fire(id_a, id_b, success=success)
686
-
687
- # Keep legacy dict in sync for any external code still using it
688
- synapse_key = tuple(sorted([id_a, id_b]))
689
- self.synapses[synapse_key] = syn
690
- self.synapse_adjacency.setdefault(synapse_key[0], [])
691
- self.synapse_adjacency.setdefault(synapse_key[1], [])
692
- if syn not in self.synapse_adjacency[synapse_key[0]]:
693
- self.synapse_adjacency[synapse_key[0]].append(syn)
694
- if syn not in self.synapse_adjacency[synapse_key[1]]:
695
- self.synapse_adjacency[synapse_key[1]].append(syn)
696
 
697
  await self._save_synapses()
698
 
@@ -712,26 +905,17 @@ class HAIMEngine:
712
  Also syncs any legacy dict entries into the index before compacting.
713
  """
714
  async with self.synapse_lock:
715
- # Sync legacy dict SynapseIndex via the public register() API
716
- # (handles tests / external code that injects into self.synapses directly)
717
- for key, syn in list(self.synapses.items()):
718
  if self._synapse_index.get(syn.neuron_a_id, syn.neuron_b_id) is None:
719
  self._synapse_index.register(syn)
720
 
721
  removed = self._synapse_index.compact(threshold)
722
 
723
- if removed:
724
- # Rebuild legacy dicts from the index
725
- self.synapses = dict(self._synapse_index.items())
726
- self.synapse_adjacency = {}
727
- for syn in self._synapse_index.values():
728
- self.synapse_adjacency.setdefault(syn.neuron_a_id, [])
729
- self.synapse_adjacency.setdefault(syn.neuron_b_id, [])
730
- self.synapse_adjacency[syn.neuron_a_id].append(syn)
731
- self.synapse_adjacency[syn.neuron_b_id].append(syn)
732
-
733
- logger.info(f"cleanup_decay: pruned {removed} synapses below {threshold}")
734
- await self._save_synapses()
735
 
736
  async def get_stats(self) -> Dict[str, Any]:
737
  """Aggregate statistics from engine components."""
@@ -936,18 +1120,9 @@ class HAIMEngine:
936
  def _load():
937
  self._synapse_index.load_from_file(self.synapse_path)
938
 
 
939
  await self._run_in_thread(_load)
940
 
941
- # Rebuild legacy dicts from SynapseIndex for backward compat
942
- async with self.synapse_lock:
943
- self.synapses = dict(self._synapse_index.items())
944
- self.synapse_adjacency = {}
945
- for syn in self._synapse_index.values():
946
- self.synapse_adjacency.setdefault(syn.neuron_a_id, [])
947
- self.synapse_adjacency.setdefault(syn.neuron_b_id, [])
948
- self.synapse_adjacency[syn.neuron_a_id].append(syn)
949
- self.synapse_adjacency[syn.neuron_b_id].append(syn)
950
-
951
  async def _save_synapses(self):
952
  """
953
  Save synapses to disk in JSONL format.
 
39
  from .synapse_index import SynapseIndex
40
  from .subconscious_ai import SubconsciousAIWorker
41
 
42
+ # Phase 5 AGI Stores
43
+ from .working_memory import WorkingMemoryService
44
+ from .episodic_store import EpisodicStoreService
45
+ from .semantic_store import SemanticStoreService
46
+
47
  # Phase 4.5: Recursive Synthesis Engine
48
  from .recursive_synthesizer import RecursiveSynthesizer, SynthesizerConfig
49
 
 
76
  persist_path: Optional[str] = None,
77
  config: Optional[HAIMConfig] = None,
78
  tier_manager: Optional[TierManager] = None,
79
+ working_memory: Optional[WorkingMemoryService] = None,
80
+ episodic_store: Optional[EpisodicStoreService] = None,
81
+ semantic_store: Optional[SemanticStoreService] = None,
82
  ):
83
  """
84
  Initialize HAIMEngine with optional dependency injection.
 
88
  persist_path: Path to memory persistence file.
89
  config: Configuration object. If None, uses global get_config().
90
  tier_manager: TierManager instance. If None, creates a new one.
91
+ working_memory: Optional Phase 5 WM service.
92
+ episodic_store: Optional Phase 5 EM service.
93
+ semantic_store: Optional Phase 5 Semantic service.
94
  """
95
  self.config = config or get_config()
96
  self.dimension = self.config.dimensionality
 
100
 
101
  # Core Components
102
  self.tier_manager = tier_manager or TierManager(config=self.config)
103
+
104
+ # Phase 5 Components
105
+ self.working_memory = working_memory
106
+ self.episodic_store = episodic_store
107
+ self.semantic_store = semantic_store
108
  self.binary_encoder = TextEncoder(self.dimension)
109
 
110
  # ── Phase 3.x: synapse raw dicts (kept for backward compat) ──
 
148
 
149
  # ── Phase 4.5: recursive synthesizer ───────────────────────────
150
  self._recursive_synthesizer: Optional[RecursiveSynthesizer] = None
151
+
152
+ # ── Phase 12.2: Contextual Topic Tracker ───────────────────────
153
+ from .topic_tracker import TopicTracker
154
+ self.topic_tracker = TopicTracker(self.config.context, self.dimension)
155
+
156
+ # ── Phase 12.3: Preference Learning ────────────────────────────
157
+ from .preference_store import PreferenceStore
158
+ self.preference_store = PreferenceStore(self.config.preference, self.dimension)
159
+
160
+ # ── Phase 13.2: Anticipatory Memory ────────────────────────────
161
+ from .anticipatory import AnticipatoryEngine
162
+ self.anticipatory_engine = AnticipatoryEngine(
163
+ self.config.anticipatory,
164
+ self._synapse_index,
165
+ self.tier_manager,
166
+ self.topic_tracker
167
+ )
168
 
169
  # Conceptual Layer (VSA Soul)
170
  data_dir = self.config.paths.data_dir
 
370
  """
371
  _is_gap_fill = metadata.get("source") == "llm_gap_fill"
372
 
373
+ # Phase 12.1: Aggressive Synapse Formation (Auto-bind).
374
+ # Fix 4: collect all bindings first, persist synapses only once at the end.
375
+ if hasattr(self.config, 'synapse') and self.config.synapse.auto_bind_on_store:
376
+ similar_nodes = await self.query(
377
+ node.content,
378
+ top_k=3,
379
+ associative_jump=False,
380
+ track_gaps=False,
381
+ )
382
+ bind_pairs = [
383
+ (node.id, neighbor_id)
384
+ for neighbor_id, similarity in similar_nodes
385
+ if neighbor_id != node.id
386
+ and similarity >= self.config.synapse.similarity_threshold
387
+ ]
388
+ if bind_pairs:
389
+ await self._auto_bind_batch(bind_pairs)
390
+
391
  self.subconscious_queue.append(node.id)
392
 
393
  if not _is_gap_fill:
 
397
  # Main store() method - Orchestration only
398
  # ==========================================================================
399
 
400
+ # Maximum allowed content length (Fix 5: input validation)
401
+ _MAX_CONTENT_LENGTH: int = 100_000
402
+
403
  @timer(STORE_DURATION_SECONDS, labels={"tier": "hot"})
404
  @traced("store_memory")
405
  async def store(
 
413
  Store new memory with holographic encoding.
414
 
415
  This method orchestrates the memory storage pipeline:
416
+ 1. Validate input
417
+ 2. Encode input content
418
+ 3. Evaluate tier placement via EIG
419
+ 4. Persist to storage
420
+ 5. Trigger post-store processing
421
 
422
  Args:
423
+ content: The text content to store. Must be non-empty and ≤100 000 chars.
424
  metadata: Optional metadata dictionary.
425
  goal_id: Optional goal identifier for context binding.
426
  project_id: Optional project identifier for isolation masking (Phase 4.1).
427
 
428
  Returns:
429
  The unique identifier of the stored memory node.
430
+
431
+ Raises:
432
+ ValueError: If content is empty or exceeds the maximum allowed length.
433
+ RuntimeError: If the engine has not been initialized via initialize().
434
  """
435
+ # Fix 5: Input validation
436
+ if not content or not content.strip():
437
+ raise ValueError("Memory content cannot be empty or whitespace-only.")
438
+ if len(content) > self._MAX_CONTENT_LENGTH:
439
+ raise ValueError(
440
+ f"Memory content is too long ({len(content):,} chars). "
441
+ f"Maximum: {self._MAX_CONTENT_LENGTH:,}."
442
+ )
443
+ if not self._initialized:
444
+ raise RuntimeError(
445
+ "HAIMEngine.initialize() must be awaited before calling store()."
446
+ )
447
+
448
  # 1. Encode input and bind goal context
449
  encoded_vec, updated_metadata = await self._encode_input(content, metadata, goal_id)
450
 
 
459
  # 3. Create and persist memory node
460
  node = await self._persist_memory(content, encoded_vec, updated_metadata)
461
 
462
+ # Phase 5.1: If agent_id in metadata, push to Working Memory and log Episode event
463
+ agent_id = updated_metadata.get("agent_id")
464
+ if agent_id:
465
+ if self.working_memory:
466
+ from .memory_model import WorkingMemoryItem
467
+ self.working_memory.push_item(
468
+ agent_id,
469
+ WorkingMemoryItem(
470
+ id=f"wm_{node.id[:8]}",
471
+ agent_id=agent_id,
472
+ created_at=datetime.utcnow(),
473
+ ttl_seconds=3600,
474
+ content=content,
475
+ kind="observation",
476
+ importance=node.epistemic_value or 0.5,
477
+ tags=updated_metadata.get("tags", []),
478
+ hdv=encoded_vec
479
+ )
480
+ )
481
+
482
+ episode_id = updated_metadata.get("episode_id")
483
+ if episode_id and self.episodic_store:
484
+ self.episodic_store.append_event(
485
+ episode_id=episode_id,
486
+ kind="observation",
487
+ content=content,
488
+ metadata=updated_metadata
489
+ )
490
+
491
  # 4. Trigger post-store processing
492
  await self._trigger_post_store(node, updated_metadata)
493
 
 
513
  if node_id in self.subconscious_queue:
514
  self.subconscious_queue.remove(node_id)
515
 
516
+ # 3. Phase 4.0: clean up via SynapseIndex (O(k)).
517
+ # Fix 2: legacy dict rebuild removed — _synapse_index is authoritative.
518
  async with self.synapse_lock:
519
  removed_count = self._synapse_index.remove_node(node_id)
520
 
 
 
 
 
 
 
 
 
 
521
  if removed_count:
522
  await self._save_synapses()
523
 
524
  return deleted
525
 
526
+ async def log_decision(self, context_text: str, outcome: float) -> None:
527
+ """
528
+ Phase 12.3: Logs a user decision or feedback context to update preference vector.
529
+ Outcome should be positive (e.g. 1.0) or negative (e.g. -1.0).
530
+ """
531
+ vec = await self._run_in_thread(self.binary_encoder.encode, context_text)
532
+ self.preference_store.log_decision(vec, outcome)
533
+
534
  async def close(self):
535
  """Perform graceful shutdown of engine components."""
536
  logger.info("Shutting down HAIMEngine...")
 
562
  chrono_weight: bool = True,
563
  chrono_lambda: float = 0.0001,
564
  include_neighbors: bool = False,
565
+ metadata_filter: Optional[Dict[str, Any]] = None,
566
+ include_cold: bool = False,
567
  ) -> List[Tuple[str, float]]:
568
  """
569
  Query memories using Hamming distance.
 
583
  Formula: Final_Score = Semantic_Similarity * (1 / (1 + lambda * Time_Delta))
584
  - chrono_lambda: Decay rate in seconds^-1 (default: 0.0001 ~ 2.7h half-life).
585
  - include_neighbors: Also fetch temporal neighbors (previous/next) for top results.
586
+ - include_cold: Include COLD tier in the search (bounded linear scan, default False).
587
+
588
+ Fix 3: Triggers anticipatory preloading (Phase 13.2) as fire-and-forget after returning.
589
  """
590
  # Encode Query
591
  query_vec = await self._run_in_thread(self.binary_encoder.encode, query_text)
592
 
593
+ # Phase 12.2: Context Tracking
594
+ is_shift, sim = self.topic_tracker.add_query(query_vec)
595
+ if is_shift:
596
+ logger.info(f"Context shifted during query. (sim {sim:.3f})")
597
+
598
  # Phase 4.1: Apply project isolation mask to query
599
  if project_id:
600
  query_vec = self.isolation_masker.apply_mask(query_vec, project_id)
 
605
  query_vec,
606
  top_k=top_k * 2,
607
  time_range=time_range,
608
+ metadata_filter=metadata_filter,
609
+ include_cold=include_cold,
610
  )
611
 
612
  scores: Dict[str, float] = {}
 
628
  if chrono_weight and score > 0:
629
  mem = mem_map.get(nid)
630
  if mem:
631
+ time_delta = max(0.0, now_ts - mem.created_at.timestamp()) # seconds since creation
632
  # Formula: Final = Semantic * (1 / (1 + lambda * time_delta))
633
  decay_factor = 1.0 / (1.0 + chrono_lambda * time_delta)
634
  score = score * decay_factor
635
 
636
+ # Phase 12.3: Preference Learning Bias
637
+ if self.preference_store.config.enabled and self.preference_store.preference_vector is not None:
638
+ mem = mem_map.get(nid)
639
+ if not mem:
640
+ mem = await self.tier_manager.get_memory(nid)
641
+ if mem and mem.id not in mem_map:
642
+ mem_map[mem.id] = mem
643
+ if mem:
644
+ score = self.preference_store.bias_score(mem.hdv, score)
645
+
646
  scores[nid] = score
647
 
648
+ # Phase 5.1: Boost context matching Working Memory
649
+ agent_id = metadata_filter.get("agent_id") if metadata_filter else None
650
+ if agent_id and self.working_memory:
651
+ wm_state = self.working_memory.get_state(agent_id)
652
+ if wm_state:
653
+ wm_texts = [item.content for item in wm_state.items]
654
+ if wm_texts:
655
+ # Very lightweight lexical boost for items currently in working memory
656
+ q_lower = query_text.lower()
657
+ for nid in scores:
658
+ mem = mem_map.get(nid) # Assuming already cached from chrono weighting
659
+ if mem and mem.content:
660
+ if any(w_text.lower() in mem.content.lower() for w_text in wm_texts):
661
+ scores[nid] *= 1.15 # 15% boost for WM overlap
662
+
663
  # 2. Associative Spreading (via SynapseIndex for O(1) adjacency lookup)
664
  if associative_jump and self._synapse_index:
665
  top_seeds = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:3]
 
678
  if neighbor not in augmented_scores:
679
  mem = await self.tier_manager.get_memory(neighbor)
680
  if mem:
681
+ if metadata_filter:
682
+ match = True
683
+ node_meta = mem.metadata or {}
684
+ for k, v in metadata_filter.items():
685
+ if node_meta.get(k) != v:
686
+ match = False
687
+ break
688
+ if not match:
689
+ continue
690
  augmented_scores[neighbor] = query_vec.similarity(mem.hdv)
691
 
692
  if neighbor in augmented_scores:
 
742
  if mem.previous_id:
743
  prev_mem = await self.tier_manager.get_memory(mem.previous_id)
744
  if prev_mem and prev_mem.id not in scores:
745
+ if metadata_filter:
746
+ match = True
747
+ p_meta = prev_mem.metadata or {}
748
+ for k, v in metadata_filter.items():
749
+ if p_meta.get(k) != v:
750
+ match = False
751
+ break
752
+ if not match:
753
+ continue
754
  neighbor_ids.add(prev_mem.id)
755
 
756
  # Try to find the memory that follows this one (has this as previous_id).
 
770
  # Re-sort after adding neighbors, but preserve query() top_k contract.
771
  top_results = sorted(top_results, key=lambda x: x[1], reverse=True)[:top_k]
772
 
773
+ # Phase 13.2 (Fix 3): Anticipatory preloading — fire-and-forget so it
774
+ # never blocks the caller. Only activated when the engine is fully warm.
775
+ if top_results and self._initialized and self.config.anticipatory.enabled:
776
+ asyncio.ensure_future(
777
+ self.anticipatory_engine.predict_and_preload(top_results[0][0])
778
+ )
779
+
780
  return top_results
781
 
782
+ async def get_context_nodes(self, top_k: int = 3) -> List[Tuple[str, float]]:
783
+ """
784
+ Phase 12.2: Contextual Awareness
785
+ Retrieves the top_k most relevant nodes relating to the current topic context vector.
786
+ Should be explicitly used by prompt builders before LLM logic injection.
787
+ """
788
+ if not self.topic_tracker.config.enabled:
789
+ return []
790
+
791
+ ctx = self.topic_tracker.get_context()
792
+ if ctx is None:
793
+ return []
794
+
795
+ results = await self.tier_manager.search(
796
+ ctx,
797
+ top_k=top_k,
798
+ time_range=None,
799
+ metadata_filter=None,
800
+ )
801
+ return results
802
  async def _background_dream(self, depth: int = 2):
803
  """
804
  Passive Subconscious – strengthen synapses in idle cycles.
 
851
 
852
  return sorted(active_nodes, key=score, reverse=True)[:max_collapse]
853
 
854
+ async def _auto_bind_batch(
855
+ self,
856
+ pairs: List[Tuple[str, str]],
857
+ success: bool = True,
858
+ weight: float = 1.0,
859
+ ) -> None:
860
+ """
861
+ Fix 4: Bind multiple (id_a, id_b) pairs in one pass, saving synapses once.
862
+
863
+ Used by auto-bind in _trigger_post_store() to avoid N disk writes per store.
864
+ """
865
+ async with self.synapse_lock:
866
+ for id_a, id_b in pairs:
867
+ mem_a = await self.tier_manager.get_memory(id_a)
868
+ mem_b = await self.tier_manager.get_memory(id_b)
869
+ if mem_a and mem_b:
870
+ self._synapse_index.add_or_fire(id_a, id_b, success=success, weight=weight)
871
+ await self._save_synapses()
872
+
873
+ async def bind_memories(self, id_a: str, id_b: str, success: bool = True, weight: float = 1.0):
874
  """
875
  Bind two memories by ID.
876
 
877
+ Fix 2: delegates exclusively to SynapseIndex legacy dict sync removed.
878
+ The legacy self.synapses / self.synapse_adjacency attributes remain for
879
+ backward compatibility but are only populated at startup from disk.
880
  """
881
  mem_a = await self.tier_manager.get_memory(id_a)
882
  mem_b = await self.tier_manager.get_memory(id_b)
 
885
  return
886
 
887
  async with self.synapse_lock:
888
+ self._synapse_index.add_or_fire(id_a, id_b, success=success, weight=weight)
 
 
 
 
 
 
 
 
 
 
889
 
890
  await self._save_synapses()
891
 
 
905
  Also syncs any legacy dict entries into the index before compacting.
906
  """
907
  async with self.synapse_lock:
908
+ # Retain legacy→index sync so tests that write to self.synapses directly
909
+ # still get their entries registered (Fix 2: sync only in this direction).
910
+ for syn in list(self.synapses.values()):
911
  if self._synapse_index.get(syn.neuron_a_id, syn.neuron_b_id) is None:
912
  self._synapse_index.register(syn)
913
 
914
  removed = self._synapse_index.compact(threshold)
915
 
916
+ if removed:
917
+ logger.info(f"cleanup_decay: pruned {removed} synapses below {threshold}")
918
+ await self._save_synapses()
 
 
 
 
 
 
 
 
 
919
 
920
  async def get_stats(self) -> Dict[str, Any]:
921
  """Aggregate statistics from engine components."""
 
1120
  def _load():
1121
  self._synapse_index.load_from_file(self.synapse_path)
1122
 
1123
+ # Fix 2: _synapse_index is authoritative — legacy dicts no longer rebuilt.
1124
  await self._run_in_thread(_load)
1125
 
 
 
 
 
 
 
 
 
 
 
1126
  async def _save_synapses(self):
1127
  """
1128
  Save synapses to disk in JSONL format.
src/mnemocore/core/episodic_store.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Episodic Store Service
3
+ ======================
4
+ Manages sequences of events (Episodes), chaining them chronologically.
5
+ Provides the foundation for episodic recall and narrative tracking over time.
6
+ """
7
+
8
+ from typing import Dict, List, Optional, Any
9
+ from datetime import datetime
10
+ import threading
11
+ import uuid
12
+ import logging
13
+
14
+ from .memory_model import Episode, EpisodeEvent
15
+ from .tier_manager import TierManager
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class EpisodicStoreService:
21
+ def __init__(self, tier_manager: Optional[TierManager] = None):
22
+ self._tier_manager = tier_manager
23
+ # In-memory index of active episodes; eventually backed by SQLite/Qdrant
24
+ self._active_episodes: Dict[str, Episode] = {}
25
+ # Simple backward index map from agent to sorted list of historical episodes
26
+ self._agent_history: Dict[str, List[Episode]] = {}
27
+ self._lock = threading.RLock()
28
+
29
+ def start_episode(
30
+ self, agent_id: str, goal: Optional[str] = None, context: Optional[str] = None
31
+ ) -> str:
32
+ with self._lock:
33
+ ep_id = f"ep_{uuid.uuid4().hex[:12]}"
34
+
35
+ # Find previous absolute episode for this agent to populate links_prev
36
+ prev_links = []
37
+ if agent_id in self._agent_history and self._agent_history[agent_id]:
38
+ last_ep = self._agent_history[agent_id][-1]
39
+ prev_links.append(last_ep.id)
40
+
41
+ new_ep = Episode(
42
+ id=ep_id,
43
+ agent_id=agent_id,
44
+ started_at=datetime.utcnow(),
45
+ ended_at=None,
46
+ goal=goal,
47
+ context=context,
48
+ events=[],
49
+ outcome="in_progress",
50
+ reward=None,
51
+ links_prev=prev_links,
52
+ links_next=[],
53
+ ltp_strength=0.0,
54
+ reliability=1.0,
55
+ )
56
+
57
+ # Link the previous episode forward
58
+ if prev_links:
59
+ last_ep_id = prev_links[0]
60
+ last_ep = self._get_historical_ep(agent_id, last_ep_id)
61
+ if last_ep and new_ep.id not in last_ep.links_next:
62
+ last_ep.links_next.append(new_ep.id)
63
+
64
+ self._active_episodes[ep_id] = new_ep
65
+ return ep_id
66
+
67
+ def append_event(
68
+ self,
69
+ episode_id: str,
70
+ kind: str,
71
+ content: str,
72
+ metadata: Optional[dict[str, Any]] = None,
73
+ ) -> None:
74
+ with self._lock:
75
+ ep = self._active_episodes.get(episode_id)
76
+ if not ep:
77
+ logger.warning(f"Attempted to append event to inactive or not found episode: {episode_id}")
78
+ return
79
+
80
+ event = EpisodeEvent(
81
+ timestamp=datetime.utcnow(),
82
+ kind=kind, # type: ignore
83
+ content=content,
84
+ metadata=metadata or {},
85
+ )
86
+ ep.events.append(event)
87
+
88
+ def end_episode(
89
+ self, episode_id: str, outcome: str, reward: Optional[float] = None
90
+ ) -> None:
91
+ with self._lock:
92
+ ep = self._active_episodes.pop(episode_id, None)
93
+ if not ep:
94
+ logger.warning(f"Attempted to end inactive or not found episode: {episode_id}")
95
+ return
96
+
97
+ ep.ended_at = datetime.utcnow()
98
+ ep.outcome = outcome # type: ignore
99
+ ep.reward = reward
100
+
101
+ agent_history = self._agent_history.setdefault(ep.agent_id, [])
102
+ agent_history.append(ep)
103
+
104
+ # Sort by start time just to ensure chronological order is preserved
105
+ agent_history.sort(key=lambda x: x.started_at)
106
+
107
+ logger.debug(f"Ended episode {episode_id} with outcome {outcome}")
108
+
109
+ def get_episode(self, episode_id: str) -> Optional[Episode]:
110
+ with self._lock:
111
+ # Check active first
112
+ if episode_id in self._active_episodes:
113
+ return self._active_episodes[episode_id]
114
+ # Then check history
115
+ for history in self._agent_history.values():
116
+ for ep in history:
117
+ if ep.id == episode_id:
118
+ return ep
119
+ return None
120
+
121
+ def get_recent(
122
+ self, agent_id: str, limit: int = 5, context: Optional[str] = None
123
+ ) -> List[Episode]:
124
+ with self._lock:
125
+ history = self._agent_history.get(agent_id, [])
126
+
127
+ # Active episodes count too
128
+ active = [ep for ep in self._active_episodes.values() if ep.agent_id == agent_id]
129
+
130
+ combined = history + active
131
+ combined.sort(key=lambda x: x.started_at, reverse=True)
132
+
133
+ if context:
134
+ combined = [ep for ep in combined if ep.context == context]
135
+
136
+ return combined[:limit]
137
+
138
+ def _get_historical_ep(self, agent_id: str, episode_id: str) -> Optional[Episode]:
139
+ history = self._agent_history.get(agent_id, [])
140
+ for ep in history:
141
+ if ep.id == episode_id:
142
+ return ep
143
+ return None
144
+
src/mnemocore/core/forgetting_curve.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Forgetting Curve Manager (Phase 5.0)
3
+ =====================================
4
+ Implements Ebbinghaus-based spaced repetition scheduling for MnemoCore.
5
+
6
+ The ForgettingCurveManager layers on top of AdaptiveDecayEngine to:
7
+ 1. Schedule "review" events at optimal intervals (spaced repetition)
8
+ 2. Decide whether low-retention memories should be consolidated vs. deleted
9
+ 3. Work collaboratively with the ConsolidationWorker
10
+
11
+ Key idea: at each review interval, the system re-evaluates a memory's EIG.
12
+ - High EIG + low retention → CONSOLIDATE (absorb into a stronger anchor)
13
+ - Low EIG + low retention → ARCHIVE / EVICT
14
+
15
+ The review scheduling uses the SuperMemo-inspired interval:
16
+ next_review_days = S_i * ln(1 / TARGET_RETENTION)^-1
17
+
18
+ where TARGET_RETENTION = 0.70 (retain 70% at next review point).
19
+
20
+ Public API:
21
+ manager = ForgettingCurveManager(engine)
22
+ await manager.run_once(nodes) # scan HOT/WARM nodes, schedule reviews
23
+ schedule = manager.get_schedule() # sorted list of upcoming reviews
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ import math
30
+ from dataclasses import dataclass, field
31
+ from datetime import datetime, timezone, timedelta
32
+ from typing import TYPE_CHECKING, Dict, List, Optional
33
+
34
+ from loguru import logger
35
+
36
+ from .temporal_decay import AdaptiveDecayEngine, get_adaptive_decay_engine
37
+
38
+ if TYPE_CHECKING:
39
+ from .node import MemoryNode
40
+
41
+
42
+ # ------------------------------------------------------------------ #
43
+ # Constants #
44
+ # ------------------------------------------------------------------ #
45
+
46
+ TARGET_RETENTION: float = 0.70 # Retention level at which we schedule the next review
47
+ MIN_EIG_TO_CONSOLIDATE: float = 0.3 # Minimum epistemic value to consolidate instead of evict
48
+
49
+
50
+ # ------------------------------------------------------------------ #
51
+ # Review Schedule Entry #
52
+ # ------------------------------------------------------------------ #
53
+
54
+ @dataclass
55
+ class ReviewEntry:
56
+ """A scheduled review for a single memory."""
57
+ memory_id: str
58
+ due_at: datetime # When to review
59
+ current_retention: float # Retention at scheduling time
60
+ stability: float # S_i at scheduling time
61
+ action: str = "review" # "review" | "consolidate" | "evict"
62
+
63
+ def to_dict(self) -> Dict:
64
+ return {
65
+ "memory_id": self.memory_id,
66
+ "due_at": self.due_at.isoformat(),
67
+ "current_retention": round(self.current_retention, 4),
68
+ "stability": round(self.stability, 4),
69
+ "action": self.action,
70
+ }
71
+
72
+
73
+ # ------------------------------------------------------------------ #
74
+ # Forgetting Curve Manager #
75
+ # ------------------------------------------------------------------ #
76
+
77
+ class ForgettingCurveManager:
78
+ """
79
+ Schedules spaced-repetition review events for MemoryNodes.
80
+
81
+ Attach to a running HAIMEngine to enable automatic review scheduling.
82
+ Works in concert with AdaptiveDecayEngine and ConsolidationWorker.
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ engine=None, # HAIMEngine – typed as Any to avoid circular import
88
+ decay_engine: Optional[AdaptiveDecayEngine] = None,
89
+ target_retention: float = TARGET_RETENTION,
90
+ min_eig_to_consolidate: float = MIN_EIG_TO_CONSOLIDATE,
91
+ ) -> None:
92
+ self.engine = engine
93
+ self.decay = decay_engine or get_adaptive_decay_engine()
94
+ self.target_retention = target_retention
95
+ self.min_eig_to_consolidate = min_eig_to_consolidate
96
+ self._schedule: List[ReviewEntry] = []
97
+
98
+ # ---- Interval calculation ------------------------------------ #
99
+
100
+ def next_review_days(self, node: "MemoryNode") -> float:
101
+ """
102
+ Days until the next review should be scheduled.
103
+
104
+ Derived from: TARGET_RETENTION = e^(-next_days / S_i)
105
+ → next_days = -S_i * ln(TARGET_RETENTION)
106
+
107
+ Example: S_i=5, target=0.70 → next_days = 5 × 0.357 ≈ 1.78 days
108
+ """
109
+ s_i = self.decay.stability(node)
110
+ # Protect against math domain errors
111
+ target = max(1e-6, min(self.target_retention, 0.999))
112
+ return -s_i * math.log(target)
113
+
114
+ def _determine_action(self, node: "MemoryNode", retention: float) -> str:
115
+ """
116
+ Decide what to do with a low-retention memory:
117
+ - consolidate: has historical importance (epistemic_value > threshold)
118
+ - evict: low value, low retention
119
+ - review: needs attention but not critical yet
120
+ """
121
+ if self.decay.should_evict(node):
122
+ eig = getattr(node, "epistemic_value", 0.0)
123
+ if eig >= self.min_eig_to_consolidate:
124
+ return "consolidate"
125
+ return "evict"
126
+ return "review"
127
+
128
+ # ---- Scan and schedule --------------------------------------- #
129
+
130
+ def schedule_reviews(self, nodes: "List[MemoryNode]") -> List[ReviewEntry]:
131
+ """
132
+ Scan the provided nodes and build a schedule of upcoming reviews.
133
+ Nodes with retention ≤ REVIEW_THRESHOLD are immediately flagged.
134
+
135
+ Returns the new ReviewEntry objects added to the schedule.
136
+ """
137
+ now = datetime.now(timezone.utc)
138
+ new_entries: List[ReviewEntry] = []
139
+
140
+ for node in nodes:
141
+ retention = self.decay.retention(node)
142
+ s_i = self.decay.stability(node)
143
+
144
+ # Always update review_candidate flag on the node itself
145
+ self.decay.update_review_candidate(node)
146
+
147
+ # Schedule next review based on spaced repetition interval
148
+ days_until = self.next_review_days(node)
149
+ due_at = now + timedelta(days=days_until)
150
+ action = self._determine_action(node, retention)
151
+
152
+ entry = ReviewEntry(
153
+ memory_id=node.id,
154
+ due_at=due_at,
155
+ current_retention=retention,
156
+ stability=s_i,
157
+ action=action,
158
+ )
159
+ new_entries.append(entry)
160
+
161
+ # Merge into the schedule (replace existing entries for same memory_id)
162
+ existing_ids = {e.memory_id for e in self._schedule}
163
+ self._schedule = [
164
+ e for e in self._schedule if e.memory_id not in {n.id for n in nodes}
165
+ ]
166
+ self._schedule.extend(new_entries)
167
+ self._schedule.sort(key=lambda e: e.due_at)
168
+
169
+ logger.info(
170
+ f"ForgettingCurveManager: scheduled {len(new_entries)} reviews for {len(nodes)} nodes. "
171
+ f"Total scheduled: {len(self._schedule)}"
172
+ )
173
+ return new_entries
174
+
175
+ def get_schedule(self) -> List[ReviewEntry]:
176
+ """Return the current review schedule sorted by due_at."""
177
+ return sorted(self._schedule, key=lambda e: e.due_at)
178
+
179
+ def get_due_reviews(self) -> List[ReviewEntry]:
180
+ """Return entries that are due now (due_at <= now)."""
181
+ now = datetime.now(timezone.utc)
182
+ return [e for e in self._schedule if e.due_at <= now]
183
+
184
+ def get_actions_by_type(self, action: str) -> List[ReviewEntry]:
185
+ """Filter schedule by action type: 'review', 'consolidate', or 'evict'."""
186
+ return [e for e in self._schedule if e.action == action]
187
+
188
+ def remove_entry(self, memory_id: str) -> None:
189
+ """Remove a memory from the review schedule (e.g., it was evicted)."""
190
+ self._schedule = [e for e in self._schedule if e.memory_id != memory_id]
191
+
192
+ # ---- Engine integration ------------------------------------- #
193
+
194
+ async def run_once(self) -> Dict:
195
+ """
196
+ Run a full scan over HOT + WARM nodes and update the review schedule.
197
+
198
+ Returns a stats dict with counts per action.
199
+ """
200
+ if self.engine is None:
201
+ logger.warning("ForgettingCurveManager: no engine attached, cannot scan tiers.")
202
+ return {}
203
+
204
+ nodes: List["MemoryNode"] = []
205
+ try:
206
+ hot = await self.engine.tier_manager.get_hot_snapshot()
207
+ nodes.extend(hot)
208
+ except Exception as e:
209
+ logger.warning(f"ForgettingCurveManager: could not fetch HOT nodes: {e}")
210
+
211
+ try:
212
+ warm = await self.engine.tier_manager.list_warm(max_results=1000)
213
+ nodes.extend(warm)
214
+ except (AttributeError, Exception) as e:
215
+ logger.debug(f"ForgettingCurveManager: WARM fetch skipped: {e}")
216
+
217
+ entries = self.schedule_reviews(nodes)
218
+
219
+ # Count actions
220
+ from collections import Counter
221
+ action_counts = dict(Counter(e.action for e in entries))
222
+
223
+ logger.info(f"ForgettingCurveManager scan: {action_counts}")
224
+ return {
225
+ "nodes_scanned": len(nodes),
226
+ "entries_scheduled": len(entries),
227
+ "action_counts": action_counts,
228
+ "timestamp": datetime.now(timezone.utc).isoformat(),
229
+ }
230
+
231
+
232
+ # Convenience import alias
233
+ from typing import Dict # noqa: E402 (already imported above, just ensuring type hint works)
src/mnemocore/core/hnsw_index.py CHANGED
@@ -50,18 +50,32 @@ FLAT_THRESHOLD: int = 256 # use flat index below this hop count
50
  # HNSW Index Manager #
51
  # ------------------------------------------------------------------ #
52
 
 
 
 
 
 
53
  class HNSWIndexManager:
54
  """
55
  Manages a FAISS HNSW binary ANN index for the HOT tier.
 
56
 
57
  Automatically switches between:
58
  - IndexBinaryFlat (N < FLAT_THRESHOLD — exact, faster for small N)
59
  - IndexBinaryHNSW (N ≥ FLAT_THRESHOLD — approx, faster for large N)
60
-
61
- The index is rebuilt from scratch when switching modes (rare operation).
62
- All operations are synchronous (called from within asyncio.Lock context).
63
  """
64
 
 
 
 
 
 
 
 
 
 
 
 
65
  def __init__(
66
  self,
67
  dimension: int = 16384,
@@ -69,52 +83,70 @@ class HNSWIndexManager:
69
  ef_construction: int = DEFAULT_EF_CONSTRUCTION,
70
  ef_search: int = DEFAULT_EF_SEARCH,
71
  ):
 
 
 
72
  self.dimension = dimension
73
  self.m = m
74
  self.ef_construction = ef_construction
75
  self.ef_search = ef_search
76
 
77
- # ID maps
78
- self._id_map: Dict[int, str] = {} # faiss_int_id → node_id
79
- self._node_map: Dict[str, int] = {} # node_id → faiss_int_id
80
- self._next_id: int = 1
81
- self._use_hnsw: bool = False
82
 
83
- # FAISS index (initialised below)
 
 
 
84
  self._index = None
 
 
 
 
 
 
 
 
85
 
86
  if FAISS_AVAILABLE:
87
- self._build_flat_index()
88
- else:
89
- logger.warning("HNSWIndexManager running WITHOUT faiss — linear fallback only.")
 
 
 
90
 
91
  # ---- Index construction -------------------------------------- #
92
 
93
  def _build_flat_index(self) -> None:
94
  """Create a fresh IndexBinaryFlat (exact Hamming ANN)."""
95
- base = faiss.IndexBinaryFlat(self.dimension)
96
- self._index = faiss.IndexBinaryIDMap(base)
97
  self._use_hnsw = False
98
  logger.debug(f"Built FAISS flat binary index (dim={self.dimension})")
99
 
100
- def _build_hnsw_index(self, existing_nodes: Optional[List[Tuple[int, np.ndarray]]] = None) -> None:
101
  """
102
  Build an HNSW binary index and optionally re-populate with existing vectors.
103
-
104
- Note: FAISS IndexBinaryHNSW does NOT support IDMap natively, so we use a
105
- custom double-mapping approach: HNSW indices map 1-to-1 to our _id_map.
106
- We rebuild as IndexBinaryHNSW and re-add all existing vectors.
107
  """
108
  hnsw = faiss.IndexBinaryHNSW(self.dimension, self.m)
109
  hnsw.hnsw.efConstruction = self.ef_construction
110
  hnsw.hnsw.efSearch = self.ef_search
111
 
112
- if existing_nodes:
113
- # Batch add in order of faiss_int_id so positions are deterministic
114
- existing_nodes.sort(key=lambda x: x[0])
115
- vecs = np.stack([v for _, v in existing_nodes])
116
- hnsw.add(vecs)
117
- logger.debug(f"HNSW index rebuilt with {len(existing_nodes)} existing vectors")
 
 
 
 
 
 
 
 
 
 
118
 
119
  self._index = hnsw
120
  self._use_hnsw = True
@@ -125,43 +157,18 @@ class HNSWIndexManager:
125
 
126
  def _maybe_upgrade_to_hnsw(self) -> None:
127
  """Upgrade to HNSW index if HOT tier has grown large enough."""
128
- if not FAISS_AVAILABLE:
129
- return
130
- if self._use_hnsw:
131
  return
132
- if len(self._id_map) < FLAT_THRESHOLD:
 
133
  return
134
 
135
  logger.info(
136
- f"HOT tier size ({len(self._id_map)}) ≥ threshold ({FLAT_THRESHOLD}) "
137
  "— upgrading to HNSW index."
138
  )
139
 
140
- # NOTE: For HNSW without IDMap we maintain position-based mapping.
141
- # We rebuild from the current flat index contents.
142
- # Collect all existing (local_pos → node_vector) pairs.
143
- #
144
- # For simplicity in this transition we do a full rebuild from scratch:
145
- # the upgrade happens at most once per process lifetime (HOT usually stays
146
- # under threshold or once it crosses, it stays crossed).
147
- existing: List[Tuple[int, np.ndarray]] = []
148
- for fid, node_id in self._id_map.items():
149
- # We can't reconstruct vectors from IndexBinaryIDMap cheaply,
150
- # so we store them in a shadow cache while using the flat index.
151
- if node_id in self._vector_cache:
152
- existing.append((fid, self._vector_cache[node_id]))
153
-
154
- self._build_hnsw_index(existing)
155
-
156
- # ---- Vector shadow cache (needed for HNSW rebuild) ----------- #
157
- # HNSW indices don't support IDMap; we cache raw vectors separately
158
- # so we can rebuild on threshold-crossing.
159
-
160
- @property
161
- def _vector_cache(self) -> Dict[str, np.ndarray]:
162
- if not hasattr(self, "_vcache"):
163
- object.__setattr__(self, "_vcache", {})
164
- return self._vcache # type: ignore[attr-defined]
165
 
166
  # ---- Public API --------------------------------------------- #
167
 
@@ -176,77 +183,66 @@ class HNSWIndexManager:
176
  if not FAISS_AVAILABLE or self._index is None:
177
  return
178
 
179
- fid = self._next_id
180
- self._next_id += 1
181
- self._id_map[fid] = node_id
182
- self._node_map[node_id] = fid
183
- self._vector_cache[node_id] = hdv_data.copy()
184
-
185
- vec = np.expand_dims(hdv_data, axis=0)
186
 
187
- try:
188
- if self._use_hnsw:
189
- # HNSW.add() — position is implicit (sequential)
190
  self._index.add(vec)
191
- else:
192
- ids = np.array([fid], dtype="int64")
193
- self._index.add_with_ids(vec, ids)
194
- except Exception as exc:
195
- logger.error(f"HNSW/FAISS add failed for {node_id}: {exc}")
196
- return
197
 
198
- # Check if we should upgrade to HNSW
199
- self._maybe_upgrade_to_hnsw()
200
 
201
  def remove(self, node_id: str) -> None:
202
  """
203
  Remove a node from the index.
204
-
205
- For HNSW (no IDMap), we mark the node as deleted in our bookkeeping
206
- and rebuild the index lazily when the deletion rate exceeds 20%.
207
  """
208
  if not FAISS_AVAILABLE or self._index is None:
209
  return
210
 
211
- fid = self._node_map.pop(node_id, None)
212
- if fid is None:
213
- return
214
-
215
- self._id_map.pop(fid, None)
216
- self._vector_cache.pop(node_id, None)
217
-
218
- if not self._use_hnsw:
219
  try:
220
- ids = np.array([fid], dtype="int64")
221
- self._index.remove_ids(ids)
222
- except Exception as exc:
223
- logger.error(f"FAISS flat remove failed for {node_id}: {exc}")
224
- else:
225
- # HNSW doesn't support removal; track stale fraction and rebuild when needed
226
- if not hasattr(self, "_stale_count"):
227
- object.__setattr__(self, "_stale_count", 0)
228
- self._stale_count += 1 # type: ignore[attr-defined]
229
-
230
- total = max(len(self._id_map) + self._stale_count, 1)
231
- stale_fraction = self._stale_count / total
232
- if stale_fraction > 0.20 and len(self._id_map) > 0:
233
- logger.info(f"HNSW stale fraction {stale_fraction:.1%} — rebuilding index.")
234
- existing = [
235
- (fid2, self._vector_cache[nid])
236
- for fid2, nid in self._id_map.items()
237
- if nid in self._vector_cache
238
- ]
239
- self._build_hnsw_index(existing)
240
- self._stale_count = 0
 
 
 
 
 
 
 
 
 
 
241
 
242
  def search(self, query_data: np.ndarray, top_k: int = 10) -> List[Tuple[str, float]]:
243
  """
244
  Search for top-k nearest neighbours.
245
 
246
- Args:
247
- query_data: Packed uint8 query array (D/8 bytes).
248
- top_k: Number of results to return.
249
-
250
  Returns:
251
  List of (node_id, similarity_score) sorted by descending similarity.
252
  similarity = 1 - normalised_hamming_distance ∈ [0, 1].
@@ -254,8 +250,25 @@ class HNSWIndexManager:
254
  if not FAISS_AVAILABLE or self._index is None or not self._id_map:
255
  return []
256
 
257
- k = min(top_k, len(self._id_map))
258
- q = np.expand_dims(query_data, axis=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  try:
261
  distances, ids = self._index.search(q, k)
@@ -265,43 +278,59 @@ class HNSWIndexManager:
265
 
266
  results: List[Tuple[str, float]] = []
267
  for dist, idx in zip(distances[0], ids[0]):
268
- if idx == -1:
269
  continue
270
-
271
- if self._use_hnsw:
272
- # HNSW returns 0-based position indices; map back through insertion order
273
- node_id = self._position_to_node_id(int(idx))
274
- else:
275
- node_id = self._id_map.get(int(idx))
276
-
277
- if node_id:
278
- sim = 1.0 - float(dist) / self.dimension
279
  results.append((node_id, sim))
 
 
280
 
281
  return results
282
 
283
- def _position_to_node_id(self, position: int) -> Optional[str]:
284
- """
285
- Map HNSW sequential position back to node_id.
286
- Positions correspond to insertion order; we track this via _position_map.
287
- """
288
- if not hasattr(self, "_position_map"):
289
- object.__setattr__(self, "_position_map", {})
290
- pm: Dict[int, str] = self._position_map # type: ignore[attr-defined]
291
-
292
- # Rebuild position map if needed (after index rebuild)
293
- if len(pm) < len(self._id_map):
294
- pm.clear()
295
- for pos, (fid, nid) in enumerate(
296
- sorted(self._id_map.items(), key=lambda x: x[0])
297
- ):
298
- pm[pos] = nid
299
-
300
- return pm.get(position)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  @property
303
  def size(self) -> int:
304
- return len(self._id_map)
305
 
306
  @property
307
  def index_type(self) -> str:
@@ -318,4 +347,5 @@ class HNSWIndexManager:
318
  "ef_construction": self.ef_construction if self._use_hnsw else None,
319
  "ef_search": self.ef_search if self._use_hnsw else None,
320
  "faiss_available": FAISS_AVAILABLE,
 
321
  }
 
50
  # HNSW Index Manager #
51
  # ------------------------------------------------------------------ #
52
 
53
+ import json
54
+ from pathlib import Path
55
+ from threading import Lock
56
+ from .config import get_config
57
+
58
  class HNSWIndexManager:
59
  """
60
  Manages a FAISS HNSW binary ANN index for the HOT tier.
61
+ Thread-safe singleton with disk persistence.
62
 
63
  Automatically switches between:
64
  - IndexBinaryFlat (N < FLAT_THRESHOLD — exact, faster for small N)
65
  - IndexBinaryHNSW (N ≥ FLAT_THRESHOLD — approx, faster for large N)
 
 
 
66
  """
67
 
68
+ _instance: "HNSWIndexManager | None" = None
69
+ _singleton_lock: Lock = Lock()
70
+
71
+ def __new__(cls, *args, **kwargs) -> "HNSWIndexManager":
72
+ with cls._singleton_lock:
73
+ if cls._instance is None:
74
+ obj = super().__new__(cls)
75
+ obj._initialized = False
76
+ cls._instance = obj
77
+ return cls._instance
78
+
79
  def __init__(
80
  self,
81
  dimension: int = 16384,
 
83
  ef_construction: int = DEFAULT_EF_CONSTRUCTION,
84
  ef_search: int = DEFAULT_EF_SEARCH,
85
  ):
86
+ if getattr(self, "_initialized", False):
87
+ return
88
+
89
  self.dimension = dimension
90
  self.m = m
91
  self.ef_construction = ef_construction
92
  self.ef_search = ef_search
93
 
94
+ self._write_lock = Lock()
 
 
 
 
95
 
96
+ self._id_map: List[Optional[str]] = []
97
+ self._vector_store: List[np.ndarray] = []
98
+ self._use_hnsw = False
99
+ self._stale_count = 0
100
  self._index = None
101
+
102
+ config = get_config()
103
+ data_dir = Path(config.paths.data_dir if hasattr(config, 'paths') else "./data")
104
+ data_dir.mkdir(parents=True, exist_ok=True)
105
+
106
+ self.INDEX_PATH = data_dir / "mnemocore_hnsw.faiss"
107
+ self.IDMAP_PATH = data_dir / "mnemocore_hnsw_idmap.json"
108
+ self.VECTOR_PATH = data_dir / "mnemocore_hnsw_vectors.npy"
109
 
110
  if FAISS_AVAILABLE:
111
+ if self.INDEX_PATH.exists() and self.IDMAP_PATH.exists() and self.VECTOR_PATH.exists():
112
+ self._load()
113
+ else:
114
+ self._build_flat_index()
115
+
116
+ self._initialized = True
117
 
118
  # ---- Index construction -------------------------------------- #
119
 
120
  def _build_flat_index(self) -> None:
121
  """Create a fresh IndexBinaryFlat (exact Hamming ANN)."""
122
+ self._index = faiss.IndexBinaryFlat(self.dimension)
 
123
  self._use_hnsw = False
124
  logger.debug(f"Built FAISS flat binary index (dim={self.dimension})")
125
 
126
+ def _build_hnsw_index(self) -> None:
127
  """
128
  Build an HNSW binary index and optionally re-populate with existing vectors.
 
 
 
 
129
  """
130
  hnsw = faiss.IndexBinaryHNSW(self.dimension, self.m)
131
  hnsw.hnsw.efConstruction = self.ef_construction
132
  hnsw.hnsw.efSearch = self.ef_search
133
 
134
+ if self._vector_store:
135
+ # Compact the index to remove None entries
136
+ compact_ids = []
137
+ compact_vecs = []
138
+ for i, node_id in enumerate(self._id_map):
139
+ if node_id is not None:
140
+ compact_ids.append(node_id)
141
+ compact_vecs.append(self._vector_store[i])
142
+
143
+ if compact_vecs:
144
+ vecs = np.stack(compact_vecs)
145
+ hnsw.add(vecs)
146
+
147
+ self._id_map = compact_ids
148
+ self._vector_store = compact_vecs
149
+ self._stale_count = 0
150
 
151
  self._index = hnsw
152
  self._use_hnsw = True
 
157
 
158
  def _maybe_upgrade_to_hnsw(self) -> None:
159
  """Upgrade to HNSW index if HOT tier has grown large enough."""
160
+ if not FAISS_AVAILABLE or self._use_hnsw:
 
 
161
  return
162
+ active_count = len(self._id_map) - self._stale_count
163
+ if active_count < FLAT_THRESHOLD:
164
  return
165
 
166
  logger.info(
167
+ f"HOT tier size ({active_count}) ≥ threshold ({FLAT_THRESHOLD}) "
168
  "— upgrading to HNSW index."
169
  )
170
 
171
+ self._build_hnsw_index()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
  # ---- Public API --------------------------------------------- #
174
 
 
183
  if not FAISS_AVAILABLE or self._index is None:
184
  return
185
 
186
+ vec = np.ascontiguousarray(np.expand_dims(hdv_data, axis=0))
 
 
 
 
 
 
187
 
188
+ with self._write_lock:
189
+ try:
 
190
  self._index.add(vec)
191
+ self._id_map.append(node_id)
192
+ self._vector_store.append(hdv_data.copy())
193
+ except Exception as exc:
194
+ logger.error(f"HNSW/FAISS add failed for {node_id}: {repr(exc)}")
195
+ return
 
196
 
197
+ self._maybe_upgrade_to_hnsw()
198
+ self._save()
199
 
200
  def remove(self, node_id: str) -> None:
201
  """
202
  Remove a node from the index.
203
+ Marks node as deleted and rebuilds index lazily when the deletion rate exceeds 20%.
 
 
204
  """
205
  if not FAISS_AVAILABLE or self._index is None:
206
  return
207
 
208
+ with self._write_lock:
 
 
 
 
 
 
 
209
  try:
210
+ fid = self._id_map.index(node_id)
211
+ self._id_map[fid] = None
212
+ self._stale_count += 1
213
+
214
+ total = max(len(self._id_map), 1)
215
+ stale_fraction = self._stale_count / total
216
+
217
+ if stale_fraction > 0.20 and len(self._id_map) > 0:
218
+ logger.info(f"HNSW stale fraction {stale_fraction:.1%} — rebuilding index.")
219
+ if self._use_hnsw:
220
+ self._build_hnsw_index()
221
+ else:
222
+ self._build_flat_index()
223
+ if self._vector_store:
224
+ compact_ids = []
225
+ compact_vecs = []
226
+ for i, nid in enumerate(self._id_map):
227
+ if nid is not None:
228
+ compact_ids.append(nid)
229
+ compact_vecs.append(self._vector_store[i])
230
+ if compact_vecs:
231
+ vecs = np.ascontiguousarray(np.stack(compact_vecs))
232
+ self._index.add(vecs)
233
+ self._id_map = compact_ids
234
+ self._vector_store = compact_vecs
235
+ self._stale_count = 0
236
+
237
+ self._save()
238
+ except ValueError:
239
+ pass
240
+
241
 
242
  def search(self, query_data: np.ndarray, top_k: int = 10) -> List[Tuple[str, float]]:
243
  """
244
  Search for top-k nearest neighbours.
245
 
 
 
 
 
246
  Returns:
247
  List of (node_id, similarity_score) sorted by descending similarity.
248
  similarity = 1 - normalised_hamming_distance ∈ [0, 1].
 
250
  if not FAISS_AVAILABLE or self._index is None or not self._id_map:
251
  return []
252
 
253
+ # Fetch more to account for deleted (None) entries
254
+ k = min(top_k + self._stale_count, len(self._id_map))
255
+ if k <= 0:
256
+ return []
257
+
258
+ index_dimension = int(getattr(self._index, "d", self.dimension) or self.dimension)
259
+ query_bytes = np.ascontiguousarray(query_data, dtype=np.uint8).reshape(-1)
260
+ expected_bytes = index_dimension // 8
261
+ if expected_bytes > 0 and query_bytes.size != expected_bytes:
262
+ logger.warning(
263
+ f"HNSW query dimension mismatch: index={index_dimension} bits ({expected_bytes} bytes), "
264
+ f"query={query_bytes.size} bytes. Adjusting query to index dimension."
265
+ )
266
+ if query_bytes.size > expected_bytes:
267
+ query_bytes = query_bytes[:expected_bytes]
268
+ else:
269
+ query_bytes = np.pad(query_bytes, (0, expected_bytes - query_bytes.size), mode="constant")
270
+
271
+ q = np.expand_dims(query_bytes, axis=0)
272
 
273
  try:
274
  distances, ids = self._index.search(q, k)
 
278
 
279
  results: List[Tuple[str, float]] = []
280
  for dist, idx in zip(distances[0], ids[0]):
281
+ if idx < 0 or idx >= len(self._id_map):
282
  continue
283
+
284
+ node_id = self._id_map[idx]
285
+ if node_id is not None:
286
+ sim = 1.0 - float(dist) / max(index_dimension, 1)
287
+ sim = float(np.clip(sim, 0.0, 1.0))
 
 
 
 
288
  results.append((node_id, sim))
289
+ if len(results) >= top_k:
290
+ break
291
 
292
  return results
293
 
294
+ def _save(self):
295
+ try:
296
+ faiss.write_index_binary(self._index, str(self.INDEX_PATH))
297
+ with open(self.IDMAP_PATH, "w") as f:
298
+ json.dump({
299
+ "id_map": self._id_map,
300
+ "use_hnsw": self._use_hnsw,
301
+ "stale_count": self._stale_count
302
+ }, f)
303
+ if self._vector_store:
304
+ np.save(str(self.VECTOR_PATH), np.stack(self._vector_store))
305
+ except Exception as e:
306
+ logger.error(f"Failed to save HNSW index state: {e}")
307
+
308
+ def _load(self):
309
+ try:
310
+ self._index = faiss.read_index_binary(str(self.INDEX_PATH))
311
+ index_dimension = int(getattr(self._index, "d", self.dimension) or self.dimension)
312
+ if index_dimension != self.dimension:
313
+ logger.warning(
314
+ f"HNSW index dimension mismatch on load: config={self.dimension}, index={index_dimension}. "
315
+ "Using index dimension."
316
+ )
317
+ self.dimension = index_dimension
318
+ with open(self.IDMAP_PATH, "r") as f:
319
+ state = json.load(f)
320
+ self._id_map = state.get("id_map", [])
321
+ self._use_hnsw = state.get("use_hnsw", False)
322
+ self._stale_count = state.get("stale_count", 0)
323
+
324
+ vecs = np.load(str(self.VECTOR_PATH))
325
+ self._vector_store = list(vecs)
326
+ logger.info("Loaded HNSW persistent state from disk")
327
+ except Exception as e:
328
+ logger.error(f"Failed to load HNSW index state: {e}")
329
+ self._build_flat_index()
330
 
331
  @property
332
  def size(self) -> int:
333
+ return len([x for x in self._id_map if x is not None])
334
 
335
  @property
336
  def index_type(self) -> str:
 
347
  "ef_construction": self.ef_construction if self._use_hnsw else None,
348
  "ef_search": self.ef_search if self._use_hnsw else None,
349
  "faiss_available": FAISS_AVAILABLE,
350
+ "stale_count": self._stale_count
351
  }
src/mnemocore/core/memory_model.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Memory Models
3
+ =============
4
+ Data classes mapping the Cognitive Architecture Phase 5 entities:
5
+ Working Memory (WM), Episodic Memory (EM), Semantic Memory (SM),
6
+ Procedural Memory (PM), and Meta-Memory (MM).
7
+ """
8
+
9
+ from dataclasses import dataclass, field
10
+ from datetime import datetime
11
+ from typing import Any, Literal, Optional, List
12
+
13
+ from .binary_hdv import BinaryHDV
14
+
15
+
16
+ # --- Working Memory (WM) ---
17
+
18
+ @dataclass
19
+ class WorkingMemoryItem:
20
+ id: str
21
+ agent_id: str
22
+ created_at: datetime
23
+ ttl_seconds: int
24
+ content: str
25
+ kind: Literal["thought", "observation", "goal", "plan_step", "action", "meta"]
26
+ importance: float
27
+ tags: List[str]
28
+ hdv: Optional[BinaryHDV] = None
29
+
30
+
31
+ @dataclass
32
+ class WorkingMemoryState:
33
+ agent_id: str
34
+ max_items: int
35
+ items: List[WorkingMemoryItem] = field(default_factory=list)
36
+
37
+
38
+ # --- Episodic Memory (EM) ---
39
+
40
+ @dataclass
41
+ class EpisodeEvent:
42
+ timestamp: datetime
43
+ kind: Literal["observation", "action", "thought", "reward", "error", "system"]
44
+ content: str
45
+ metadata: dict[str, Any]
46
+ hdv: Optional[BinaryHDV] = None
47
+
48
+
49
+ @dataclass
50
+ class Episode:
51
+ id: str
52
+ agent_id: str
53
+ started_at: datetime
54
+ ended_at: Optional[datetime]
55
+ goal: Optional[str]
56
+ context: Optional[str]
57
+ events: List[EpisodeEvent]
58
+ outcome: Literal["success", "failure", "partial", "unknown", "in_progress"]
59
+ reward: Optional[float]
60
+ links_prev: List[str]
61
+ links_next: List[str]
62
+ ltp_strength: float
63
+ reliability: float
64
+
65
+ @property
66
+ def is_active(self) -> bool:
67
+ return self.ended_at is None
68
+
69
+
70
+ # --- Semantic Memory (SM) ---
71
+
72
+ @dataclass
73
+ class SemanticConcept:
74
+ id: str
75
+ label: str
76
+ description: str
77
+ tags: List[str]
78
+ prototype_hdv: BinaryHDV
79
+ support_episode_ids: List[str]
80
+ reliability: float
81
+ last_updated_at: datetime
82
+ metadata: dict[str, Any]
83
+
84
+
85
+ # --- Procedural Memory (PM) ---
86
+
87
+ @dataclass
88
+ class ProcedureStep:
89
+ order: int
90
+ instruction: str
91
+ code_snippet: Optional[str] = None
92
+ tool_call: Optional[dict[str, Any]] = None
93
+
94
+
95
+ @dataclass
96
+ class Procedure:
97
+ id: str
98
+ name: str
99
+ description: str
100
+ created_by_agent: Optional[str]
101
+ created_at: datetime
102
+ updated_at: datetime
103
+ steps: List[ProcedureStep]
104
+ trigger_pattern: str
105
+ success_count: int
106
+ failure_count: int
107
+ reliability: float
108
+ tags: List[str]
109
+
110
+
111
+ # --- Meta-Memory (MM) ---
112
+
113
+ @dataclass
114
+ class SelfMetric:
115
+ name: str
116
+ value: float
117
+ window: str # e.g. "5m", "1h", "24h"
118
+ updated_at: datetime
119
+
120
+
121
+ @dataclass
122
+ class SelfImprovementProposal:
123
+ id: str
124
+ created_at: datetime
125
+ author: Literal["system", "agent", "human"]
126
+ title: str
127
+ description: str
128
+ rationale: str
129
+ expected_effect: str
130
+ status: Literal["pending", "accepted", "rejected", "implemented"]
131
+ metadata: dict[str, Any]
132
+
src/mnemocore/core/meta_memory.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Meta Memory Service
3
+ ===================
4
+ Maintains a self-model of the memory substrate, gathering metrics and surfacing self-improvement proposals.
5
+ Plays a crucial role in enabling an AGI system to observe and upgrade its own thinking architectures over time.
6
+ """
7
+
8
+ from typing import Dict, List, Optional
9
+ import threading
10
+ import logging
11
+ from datetime import datetime
12
+
13
+ from .memory_model import SelfMetric, SelfImprovementProposal
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class MetaMemoryService:
19
+ def __init__(self):
20
+ self._metrics: List[SelfMetric] = []
21
+ self._proposals: Dict[str, SelfImprovementProposal] = {}
22
+ self._lock = threading.RLock()
23
+
24
+ def record_metric(self, name: str, value: float, window: str) -> None:
25
+ """Log a new performance or algorithmic metric reading."""
26
+ with self._lock:
27
+ # We strictly bind this to metrics history for Subconscious AI trend analysis.
28
+ metric = SelfMetric(
29
+ name=name, value=value, window=window, updated_at=datetime.utcnow()
30
+ )
31
+ self._metrics.append(metric)
32
+
33
+ # Cap local metrics storage bounds
34
+ if len(self._metrics) > 10000:
35
+ self._metrics = self._metrics[-5000:]
36
+
37
+ logger.debug(f"Recorded meta-metric: {name}={value} ({window})")
38
+
39
+ def list_metrics(self, limit: int = 100, window: Optional[str] = None) -> List[SelfMetric]:
40
+ """Fetch historical metric footprints."""
41
+ with self._lock:
42
+ filtered = [m for m in self._metrics if (not window) or m.window == window]
43
+ filtered.sort(key=lambda x: x.updated_at, reverse=True)
44
+ return filtered[:limit]
45
+
46
+ def create_proposal(self, proposal: SelfImprovementProposal) -> str:
47
+ """Inject a formally modeled improvement prompt into the queue."""
48
+ with self._lock:
49
+ self._proposals[proposal.id] = proposal
50
+ logger.info(f"New self-improvement proposal created by {proposal.author}: {proposal.title}")
51
+ return proposal.id
52
+
53
+ def update_proposal_status(self, proposal_id: str, status: str) -> None:
54
+ """Mark a proposal as accepted, rejected, or implemented by the oversight entity."""
55
+ with self._lock:
56
+ proposal = self._proposals.get(proposal_id)
57
+ if not proposal:
58
+ logger.warning(f"Could not update unknown proposal ID: {proposal_id}")
59
+ return
60
+
61
+ proposal.status = status # type: ignore
62
+ logger.info(f"Proposal {proposal_id} status escalated to: {status}")
63
+
64
+ def list_proposals(self, status: Optional[str] = None) -> List[SelfImprovementProposal]:
65
+ """Retrieve proposals matching a given state."""
66
+ with self._lock:
67
+ if status:
68
+ return [p for p in self._proposals.values() if p.status == status]
69
+ return list(self._proposals.values())
70
+
src/mnemocore/core/node.py CHANGED
@@ -1,11 +1,16 @@
 
 
1
  from dataclasses import dataclass, field
2
  from datetime import datetime, timezone
3
- from typing import Dict, Any, Optional
4
  import math
5
 
6
  from .binary_hdv import BinaryHDV
7
  from .config import get_config
8
 
 
 
 
9
 
10
  @dataclass
11
  class MemoryNode:
@@ -35,6 +40,15 @@ class MemoryNode:
35
  # Phase 4.3: Episodic Chaining - links to temporally adjacent memories
36
  previous_id: Optional[str] = None # UUID of the memory created immediately before this one
37
 
 
 
 
 
 
 
 
 
 
38
  def access(self, update_weights: bool = True):
39
  """Retrieve memory (reconsolidation)"""
40
  now = datetime.now(timezone.utc)
@@ -46,6 +60,11 @@ class MemoryNode:
46
  # We recalculate based on new access count
47
  self.calculate_ltp()
48
 
 
 
 
 
 
49
  # Legacy updates
50
  self.epistemic_value *= 1.01
51
  self.epistemic_value = min(self.epistemic_value, 1.0)
 
1
+ from __future__ import annotations
2
+
3
  from dataclasses import dataclass, field
4
  from datetime import datetime, timezone
5
+ from typing import TYPE_CHECKING, Dict, Any, Optional
6
  import math
7
 
8
  from .binary_hdv import BinaryHDV
9
  from .config import get_config
10
 
11
+ if TYPE_CHECKING:
12
+ from .provenance import ProvenanceRecord
13
+
14
 
15
  @dataclass
16
  class MemoryNode:
 
40
  # Phase 4.3: Episodic Chaining - links to temporally adjacent memories
41
  previous_id: Optional[str] = None # UUID of the memory created immediately before this one
42
 
43
+ # Phase 5.0 — Agent 1: Trust & Provenance
44
+ provenance: Optional["ProvenanceRecord"] = field(default=None, repr=False)
45
+
46
+ # Phase 5.0 — Agent 2: Adaptive Temporal Decay
47
+ # Per-memory stability: S_i = S_base * (1 + k * access_count)
48
+ # Starts at 1.0; increases logarithmically on access.
49
+ stability: float = 1.0
50
+ review_candidate: bool = False # Set by ForgettingCurveManager when near decay threshold
51
+
52
  def access(self, update_weights: bool = True):
53
  """Retrieve memory (reconsolidation)"""
54
  now = datetime.now(timezone.utc)
 
60
  # We recalculate based on new access count
61
  self.calculate_ltp()
62
 
63
+ # Phase 5.0: update per-memory stability on each successful access
64
+ # S_i grows logarithmically so older frequently-accessed memories are more stable
65
+ import math as _math
66
+ self.stability = max(1.0, 1.0 + _math.log1p(self.access_count) * 0.5)
67
+
68
  # Legacy updates
69
  self.epistemic_value *= 1.01
70
  self.epistemic_value = min(self.epistemic_value, 1.0)
src/mnemocore/core/prediction_store.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Prediction Memory Store (Phase 5.0 — Agent 4)
3
+ ==============================================
4
+ Stores explicitly made predictions about future events and tracks their outcomes.
5
+
6
+ A prediction has a lifecycle:
7
+ pending → verified (correct) OR falsified (wrong) OR expired (deadline passed)
8
+
9
+ Key behaviors:
10
+ - Verified predictions STRENGTHEN related strategic memories via synaptic binding
11
+ - Falsified predictions REDUCE confidence on related memories + generate a
12
+ "lesson learned" via SubconsciousAI
13
+ - Expired predictions are flagged for manual review
14
+
15
+ Backed by a lightweight in-memory + provenance-attached store.
16
+ For persistence, predictions are serialized to node.metadata["prediction"] and
17
+ stored as regular MemoryNodes in the HOT tier with a special tag.
18
+
19
+ Public API:
20
+ store = PredictionStore()
21
+ pred_id = store.create(content="...", confidence=0.7, deadline_days=90)
22
+ store.verify(pred_id, success=True, notes="EU AI Act enforced")
23
+ due = store.get_due() # predictions past their deadline
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import uuid
29
+ from dataclasses import dataclass, field
30
+ from datetime import datetime, timezone, timedelta
31
+ from typing import Any, Dict, List, Optional
32
+
33
+ from loguru import logger
34
+
35
+
36
+ # ------------------------------------------------------------------ #
37
+ # Prediction status constants #
38
+ # ------------------------------------------------------------------ #
39
+
40
+ STATUS_PENDING = "pending"
41
+ STATUS_VERIFIED = "verified"
42
+ STATUS_FALSIFIED = "falsified"
43
+ STATUS_EXPIRED = "expired"
44
+
45
+
46
+ # ------------------------------------------------------------------ #
47
+ # PredictionRecord #
48
+ # ------------------------------------------------------------------ #
49
+
50
+ @dataclass
51
+ class PredictionRecord:
52
+ """A single forward-looking prediction stored in MnemoCore."""
53
+
54
+ id: str = field(default_factory=lambda: f"pred_{uuid.uuid4().hex[:16]}")
55
+ content: str = ""
56
+ predicted_at: str = field(
57
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
58
+ )
59
+ verification_deadline: Optional[str] = None # ISO datetime string
60
+ confidence_at_creation: float = 0.5
61
+ status: str = STATUS_PENDING
62
+ outcome: Optional[bool] = None # True=verified, False=falsified
63
+ verification_notes: Optional[str] = None
64
+ verified_at: Optional[str] = None
65
+ related_memory_ids: List[str] = field(default_factory=list)
66
+ tags: List[str] = field(default_factory=list)
67
+
68
+ def is_expired(self) -> bool:
69
+ """True if the deadline has passed and status is still pending."""
70
+ if self.status != STATUS_PENDING or self.verification_deadline is None:
71
+ return False
72
+ deadline = datetime.fromisoformat(self.verification_deadline)
73
+ if deadline.tzinfo is None:
74
+ deadline = deadline.replace(tzinfo=timezone.utc)
75
+ return datetime.now(timezone.utc) > deadline
76
+
77
+ def to_dict(self) -> Dict[str, Any]:
78
+ return {
79
+ "id": self.id,
80
+ "content": self.content,
81
+ "predicted_at": self.predicted_at,
82
+ "verification_deadline": self.verification_deadline,
83
+ "confidence_at_creation": round(self.confidence_at_creation, 4),
84
+ "status": self.status,
85
+ "outcome": self.outcome,
86
+ "verification_notes": self.verification_notes,
87
+ "verified_at": self.verified_at,
88
+ "related_memory_ids": self.related_memory_ids,
89
+ "tags": self.tags,
90
+ }
91
+
92
+ @classmethod
93
+ def from_dict(cls, d: Dict[str, Any]) -> "PredictionRecord":
94
+ return cls(
95
+ id=d.get("id", f"pred_{uuid.uuid4().hex[:16]}"),
96
+ content=d.get("content", ""),
97
+ predicted_at=d.get("predicted_at", datetime.now(timezone.utc).isoformat()),
98
+ verification_deadline=d.get("verification_deadline"),
99
+ confidence_at_creation=d.get("confidence_at_creation", 0.5),
100
+ status=d.get("status", STATUS_PENDING),
101
+ outcome=d.get("outcome"),
102
+ verification_notes=d.get("verification_notes"),
103
+ verified_at=d.get("verified_at"),
104
+ related_memory_ids=d.get("related_memory_ids", []),
105
+ tags=d.get("tags", []),
106
+ )
107
+
108
+
109
+ # ------------------------------------------------------------------ #
110
+ # PredictionStore #
111
+ # ------------------------------------------------------------------ #
112
+
113
+ class PredictionStore:
114
+ """
115
+ In-memory store for PredictionRecords with lifecycle management.
116
+
117
+ For production use, wire to an engine so verified/falsified predictions
118
+ can update related MemoryNode synapses and generate LLM insights.
119
+ """
120
+
121
+ def __init__(self, engine=None) -> None:
122
+ self.engine = engine
123
+ self._records: Dict[str, PredictionRecord] = {}
124
+
125
+ # ---- CRUD ---------------------------------------------------- #
126
+
127
+ def create(
128
+ self,
129
+ content: str,
130
+ confidence: float = 0.5,
131
+ deadline_days: Optional[float] = None,
132
+ deadline: Optional[datetime] = None,
133
+ related_memory_ids: Optional[List[str]] = None,
134
+ tags: Optional[List[str]] = None,
135
+ ) -> str:
136
+ """
137
+ Store a new prediction.
138
+
139
+ Args:
140
+ content: The prediction statement.
141
+ confidence: Confidence at creation time [0, 1].
142
+ deadline_days: Days from now until deadline (alternative to deadline).
143
+ deadline: Explicit deadline datetime (overrides deadline_days).
144
+ related_memory_ids: IDs of memories this prediction relates to.
145
+ tags: Optional classification tags.
146
+
147
+ Returns:
148
+ The prediction ID.
149
+ """
150
+ deadline_iso: Optional[str] = None
151
+ if deadline is not None:
152
+ if deadline.tzinfo is None:
153
+ deadline = deadline.replace(tzinfo=timezone.utc)
154
+ deadline_iso = deadline.isoformat()
155
+ elif deadline_days is not None:
156
+ deadline_iso = (
157
+ datetime.now(timezone.utc) + timedelta(days=deadline_days)
158
+ ).isoformat()
159
+
160
+ rec = PredictionRecord(
161
+ content=content,
162
+ confidence_at_creation=max(0.0, min(1.0, confidence)),
163
+ verification_deadline=deadline_iso,
164
+ related_memory_ids=related_memory_ids or [],
165
+ tags=tags or [],
166
+ )
167
+ self._records[rec.id] = rec
168
+ logger.info(
169
+ f"Prediction created: {rec.id} | confidence={confidence:.2f} | "
170
+ f"deadline={deadline_iso or 'none'}"
171
+ )
172
+ return rec.id
173
+
174
+ def get(self, pred_id: str) -> Optional[PredictionRecord]:
175
+ return self._records.get(pred_id)
176
+
177
+ def list_all(self, status: Optional[str] = None) -> List[PredictionRecord]:
178
+ """Return all predictions, optionally filtered by status."""
179
+ recs = list(self._records.values())
180
+ if status:
181
+ recs = [r for r in recs if r.status == status]
182
+ return sorted(recs, key=lambda r: r.predicted_at, reverse=True)
183
+
184
+ def get_due(self) -> List[PredictionRecord]:
185
+ """Return pending predictions that have passed their deadline."""
186
+ return [r for r in self._records.values() if r.is_expired()]
187
+
188
+ # ---- Lifecycle ----------------------------------------------- #
189
+
190
+ async def verify(
191
+ self,
192
+ pred_id: str,
193
+ success: bool,
194
+ notes: Optional[str] = None,
195
+ ) -> Optional[PredictionRecord]:
196
+ """
197
+ Verify or falsify a prediction.
198
+
199
+ Side effects:
200
+ - Verified: strengthens related memories via synaptic binding
201
+ - Falsified: reduces confidence on related memories + lesson learned
202
+ """
203
+ rec = self._records.get(pred_id)
204
+ if rec is None:
205
+ logger.warning(f"PredictionStore.verify: unknown id {pred_id!r}")
206
+ return None
207
+
208
+ rec.status = STATUS_VERIFIED if success else STATUS_FALSIFIED
209
+ rec.outcome = success
210
+ rec.verification_notes = notes
211
+ rec.verified_at = datetime.now(timezone.utc).isoformat()
212
+
213
+ logger.info(
214
+ f"Prediction {pred_id} → {rec.status} | notes={notes or '—'}"
215
+ )
216
+
217
+ if self.engine is not None:
218
+ if success:
219
+ await self._strengthen_related(rec)
220
+ else:
221
+ await self._weaken_related(rec)
222
+ await self._generate_lesson(rec)
223
+
224
+ return rec
225
+
226
+ async def expire_due(self) -> List[PredictionRecord]:
227
+ """Mark overdue pending predictions as expired. Returns expired list."""
228
+ due = self.get_due()
229
+ for rec in due:
230
+ rec.status = STATUS_EXPIRED
231
+ logger.info(f"Prediction {rec.id} expired (deadline passed).")
232
+ return due
233
+
234
+ # ---- Engine integration -------------------------------------- #
235
+
236
+ async def _strengthen_related(self, rec: PredictionRecord) -> None:
237
+ """Verified prediction → strengthen synapses on related memories."""
238
+ for mem_id in rec.related_memory_ids:
239
+ try:
240
+ node = await self.engine.get_memory(mem_id)
241
+ if node:
242
+ si = getattr(self.engine, "synapse_index", None)
243
+ if si:
244
+ si.add_or_strengthen(rec.id, mem_id, delta=0.15)
245
+ logger.debug(f"Prediction {rec.id}: strengthened memory {mem_id[:8]}")
246
+ except Exception as exc:
247
+ logger.debug(f"Prediction strengthen failed for {mem_id}: {exc}")
248
+
249
+ async def _weaken_related(self, rec: PredictionRecord) -> None:
250
+ """Falsified prediction → reduce confidence on related memories."""
251
+ for mem_id in rec.related_memory_ids:
252
+ try:
253
+ node = await self.engine.get_memory(mem_id)
254
+ if node:
255
+ from .bayesian_ltp import get_bayesian_updater
256
+ updater = get_bayesian_updater()
257
+ updater.observe_node_retrieval(node, helpful=False, eig_signal=0.5)
258
+ logger.debug(f"Prediction {rec.id}: weakened memory {mem_id[:8]}")
259
+ except Exception as exc:
260
+ logger.debug(f"Prediction weaken failed for {mem_id}: {exc}")
261
+
262
+ async def _generate_lesson(self, rec: PredictionRecord) -> None:
263
+ """Ask SubconsciousAI to synthesize a 'lesson learned' for a falsified prediction."""
264
+ try:
265
+ subcon = getattr(self.engine, "subconscious_ai", None)
266
+ if subcon is None:
267
+ return
268
+ prompt = (
269
+ f"The following prediction was FALSIFIED: '{rec.content}'. "
270
+ f"Confidence at creation: {rec.confidence_at_creation:.2f}. "
271
+ f"Notes: {rec.verification_notes or 'none'}. "
272
+ "In 1-2 sentences, what is the key lesson learned from this failure?"
273
+ )
274
+ lesson = await subcon.generate(prompt, max_tokens=128)
275
+ # Store the lesson as a new memory
276
+ await self.engine.store(
277
+ lesson.strip(),
278
+ metadata={
279
+ "type": "lesson_learned",
280
+ "source_prediction_id": rec.id,
281
+ "domain": "strategic",
282
+ }
283
+ )
284
+ logger.info(f"Lesson learned generated for falsified prediction {rec.id}")
285
+ except Exception as exc:
286
+ logger.debug(f"Lesson generation failed: {exc}")
287
+
288
+ # ---- Serialization ------------------------------------------- #
289
+
290
+ def to_list(self) -> List[Dict[str, Any]]:
291
+ return [r.to_dict() for r in self._records.values()]
292
+
293
+ def __len__(self) -> int:
294
+ return len(self._records)
src/mnemocore/core/preference_store.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from loguru import logger
3
+ from typing import List, Optional
4
+
5
+ from .binary_hdv import BinaryHDV, majority_bundle
6
+ from .config import PreferenceConfig
7
+
8
+ class PreferenceStore:
9
+ """
10
+ Phase 12.3: Preference Learning
11
+ Maintains a persistent vector representing implicit user preferences
12
+ based on logged decisions or positive feedback.
13
+ """
14
+ def __init__(self, config: PreferenceConfig, dimension: int):
15
+ self.config = config
16
+ self.dimension = dimension
17
+ # The preference vector represents the "ideal" or "preferred" region
18
+ self.preference_vector: Optional[BinaryHDV] = None
19
+ self.decision_history: List[BinaryHDV] = []
20
+
21
+ def log_decision(self, context_hdv: BinaryHDV, outcome: float) -> None:
22
+ """
23
+ Logs a decision or feedback event.
24
+ `outcome`: positive value (e.g. 1.0) for good feedback, negative (-1.0) for bad feedback.
25
+ If outcome is positive, the preference vector shifts slightly toward `context_hdv`.
26
+ If outcome is negative, the preference vector shifts away (invert context_hdv).
27
+ """
28
+ if not self.config.enabled:
29
+ return
30
+
31
+ target_hdv = context_hdv if outcome >= 0 else context_hdv.invert()
32
+ self.decision_history.append(target_hdv)
33
+
34
+ # Maintain history size
35
+ if len(self.decision_history) > self.config.history_limit:
36
+ self.decision_history.pop(0)
37
+
38
+ # Update preference vector via majority bundling of recent positive shifts
39
+ self.preference_vector = majority_bundle(self.decision_history)
40
+ logger.debug(f"Logged decision (outcome={outcome:.2f}). Preference vector updated.")
41
+
42
+ def bias_score(self, target_hdv: BinaryHDV, base_score: float) -> float:
43
+ """
44
+ Biases a retrieval score using the preference vector if one exists.
45
+ Formula: new_score = base_score + (learning_rate * preference_similarity)
46
+ """
47
+ if not self.config.enabled or self.preference_vector is None:
48
+ return base_score
49
+
50
+ pref_sim = self.preference_vector.similarity(target_hdv)
51
+
52
+ # We apply the learning_rate as the max potential boost an item can get from mapping exactly to preferences
53
+ return base_score + (self.config.learning_rate * pref_sim)
src/mnemocore/core/procedural_store.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Procedural Store Service
3
+ ========================
4
+ Manages actionable skills, procedural routines, and agentic workflows.
5
+ Validates triggering patterns and tracks execution success rates dynamically.
6
+ """
7
+
8
+ from typing import Dict, List, Optional
9
+ import threading
10
+ import logging
11
+ from datetime import datetime
12
+
13
+ from .memory_model import Procedure
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ProceduralStoreService:
19
+ def __init__(self):
20
+ # Local dictionary for Procedures mapping by ID
21
+ # Would typically be serialized to SQLite, JSON, or Qdrant for retrieval.
22
+ self._procedures: Dict[str, Procedure] = {}
23
+ self._lock = threading.RLock()
24
+
25
+ def store_procedure(self, proc: Procedure) -> None:
26
+ """Save a new or refined procedure into memory."""
27
+ with self._lock:
28
+ proc.updated_at = datetime.utcnow()
29
+ self._procedures[proc.id] = proc
30
+ logger.info(f"Stored procedure {proc.id} ('{proc.name}')")
31
+
32
+ def get_procedure(self, proc_id: str) -> Optional[Procedure]:
33
+ """Retrieve a procedure by exact ID."""
34
+ with self._lock:
35
+ return self._procedures.get(proc_id)
36
+
37
+ def find_applicable_procedures(
38
+ self, query: str, agent_id: Optional[str] = None, top_k: int = 5
39
+ ) -> List[Procedure]:
40
+ """
41
+ Find procedures whose trigger tags or trigger pattern matches the user intent.
42
+ Simple local text-matching for the prototype layout.
43
+ """
44
+ with self._lock:
45
+ q_lower = query.lower()
46
+ results = []
47
+ for proc in self._procedures.values():
48
+ # Prefer procedures meant directly for this agent, or system globals
49
+ if proc.created_by_agent is not None and agent_id and proc.created_by_agent != agent_id:
50
+ continue
51
+
52
+ if proc.trigger_pattern.lower() in q_lower or any(t.lower() in q_lower for t in proc.tags):
53
+ results.append(proc)
54
+
55
+ # Sort by reliability and usage history to surface most competent tools
56
+ results.sort(key=lambda p: (p.reliability, p.success_count), reverse=True)
57
+ return results[:top_k]
58
+
59
+ def record_procedure_outcome(self, proc_id: str, success: bool) -> None:
60
+ """Update procedure success metrics, affecting overall reliability."""
61
+ with self._lock:
62
+ proc = self._procedures.get(proc_id)
63
+ if not proc:
64
+ return
65
+
66
+ proc.updated_at = datetime.utcnow()
67
+ if success:
68
+ proc.success_count += 1
69
+ # Increase reliability slightly on success
70
+ proc.reliability = min(1.0, proc.reliability + 0.05)
71
+ else:
72
+ proc.failure_count += 1
73
+ # Decrease reliability heavily on failure
74
+ proc.reliability = max(0.0, proc.reliability - 0.1)
75
+
76
+ logger.debug(f"Procedure {proc_id} outcome recorded: success={success}, new rel={proc.reliability:.2f}")
77
+
src/mnemocore/core/provenance.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Provenance Tracking Module (Phase 5.0)
3
+ =======================================
4
+ W3C PROV-inspired source tracking for MnemoCore memories.
5
+
6
+ Tracks the full lifecycle of every MemoryNode:
7
+ - origin: where/how the memory was created
8
+ - lineage: ordered list of transformation events
9
+ - version: incremented on each significant mutation
10
+
11
+ This is the foundation for:
12
+ - Trust & audit trails (AI Governance)
13
+ - Contradiction resolution
14
+ - Memory-as-a-Service lineage API
15
+ - Source reliability scoring
16
+
17
+ Public API:
18
+ record = ProvenanceRecord.new(origin_type="observation", agent_id="agent-001")
19
+ record.add_event("consolidated", source_memories=["mem_a", "mem_b"])
20
+ serialized = record.to_dict()
21
+ restored = ProvenanceRecord.from_dict(serialized)
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime, timezone
28
+ from typing import Any, Dict, List, Optional
29
+
30
+
31
+ # ------------------------------------------------------------------ #
32
+ # Origin types #
33
+ # ------------------------------------------------------------------ #
34
+
35
+ ORIGIN_TYPES = {
36
+ "observation", # Direct input from agent or user
37
+ "inference", # Derived/reasoned by LLM or engine
38
+ "dream", # Produced by SubconsciousAI dream cycle
39
+ "consolidation", # Result of SemanticConsolidation merge
40
+ "external_sync", # Fetched from external source (RSS, API, etc.)
41
+ "user_correction", # Explicit user override
42
+ "prediction", # Stored as a future prediction
43
+ }
44
+
45
+
46
+ # ------------------------------------------------------------------ #
47
+ # Lineage event #
48
+ # ------------------------------------------------------------------ #
49
+
50
+ @dataclass
51
+ class LineageEvent:
52
+ """
53
+ A single step in a memory's transformation history.
54
+
55
+ Examples:
56
+ created – initial storage
57
+ accessed – retrieved by a query
58
+ consolidated – merged into or from a proto-memory cluster
59
+ verified – reliability confirmed externally
60
+ contradicted – flagged as contradicting another memory
61
+ updated – content or metadata modified
62
+ archived – moved to COLD tier
63
+ expired – TTL reached or evicted
64
+ """
65
+ event: str
66
+ timestamp: str # ISO 8601
67
+ actor: Optional[str] = None # agent_id, "system", "user", etc.
68
+ source_memories: List[str] = field(default_factory=list) # for consolidation
69
+ outcome: Optional[bool] = None # for verification events
70
+ notes: Optional[str] = None
71
+ extra: Dict[str, Any] = field(default_factory=dict)
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ d: Dict[str, Any] = {
75
+ "event": self.event,
76
+ "timestamp": self.timestamp,
77
+ }
78
+ if self.actor is not None:
79
+ d["actor"] = self.actor
80
+ if self.source_memories:
81
+ d["source_memories"] = self.source_memories
82
+ if self.outcome is not None:
83
+ d["outcome"] = self.outcome
84
+ if self.notes:
85
+ d["notes"] = self.notes
86
+ if self.extra:
87
+ d["extra"] = self.extra
88
+ return d
89
+
90
+ @classmethod
91
+ def from_dict(cls, d: Dict[str, Any]) -> "LineageEvent":
92
+ return cls(
93
+ event=d["event"],
94
+ timestamp=d["timestamp"],
95
+ actor=d.get("actor"),
96
+ source_memories=d.get("source_memories", []),
97
+ outcome=d.get("outcome"),
98
+ notes=d.get("notes"),
99
+ extra=d.get("extra", {}),
100
+ )
101
+
102
+
103
+ # ------------------------------------------------------------------ #
104
+ # Origin #
105
+ # ------------------------------------------------------------------ #
106
+
107
+ @dataclass
108
+ class ProvenanceOrigin:
109
+ """Where/how a memory was first created."""
110
+
111
+ type: str # One of ORIGIN_TYPES
112
+ agent_id: Optional[str] = None
113
+ session_id: Optional[str] = None
114
+ source_url: Optional[str] = None # For external_sync
115
+ timestamp: str = field(
116
+ default_factory=lambda: datetime.now(timezone.utc).isoformat()
117
+ )
118
+
119
+ def to_dict(self) -> Dict[str, Any]:
120
+ d: Dict[str, Any] = {
121
+ "type": self.type,
122
+ "timestamp": self.timestamp,
123
+ }
124
+ if self.agent_id:
125
+ d["agent_id"] = self.agent_id
126
+ if self.session_id:
127
+ d["session_id"] = self.session_id
128
+ if self.source_url:
129
+ d["source_url"] = self.source_url
130
+ return d
131
+
132
+ @classmethod
133
+ def from_dict(cls, d: Dict[str, Any]) -> "ProvenanceOrigin":
134
+ return cls(
135
+ type=d.get("type", "observation"),
136
+ agent_id=d.get("agent_id"),
137
+ session_id=d.get("session_id"),
138
+ source_url=d.get("source_url"),
139
+ timestamp=d.get("timestamp", datetime.now(timezone.utc).isoformat()),
140
+ )
141
+
142
+
143
+ # ------------------------------------------------------------------ #
144
+ # ProvenanceRecord — the full provenance object on a MemoryNode #
145
+ # ------------------------------------------------------------------ #
146
+
147
+ @dataclass
148
+ class ProvenanceRecord:
149
+ """
150
+ Full provenance object attached to a MemoryNode.
151
+
152
+ Designed to be serialized into node.metadata["provenance"] for
153
+ backward compatibility with existing storage layers.
154
+ """
155
+
156
+ origin: ProvenanceOrigin
157
+ lineage: List[LineageEvent] = field(default_factory=list)
158
+ version: int = 1
159
+ confidence_source: str = "bayesian_ltp" # How the confidence score is derived
160
+
161
+ # ---- Factory methods ------------------------------------------ #
162
+
163
+ @classmethod
164
+ def new(
165
+ cls,
166
+ origin_type: str = "observation",
167
+ agent_id: Optional[str] = None,
168
+ session_id: Optional[str] = None,
169
+ source_url: Optional[str] = None,
170
+ actor: Optional[str] = None,
171
+ ) -> "ProvenanceRecord":
172
+ """Create a fresh ProvenanceRecord and log the 'created' event."""
173
+ now = datetime.now(timezone.utc).isoformat()
174
+ origin = ProvenanceOrigin(
175
+ type=origin_type if origin_type in ORIGIN_TYPES else "observation",
176
+ agent_id=agent_id,
177
+ session_id=session_id,
178
+ source_url=source_url,
179
+ timestamp=now,
180
+ )
181
+ record = cls(origin=origin)
182
+ record.add_event(
183
+ event="created",
184
+ actor=actor or agent_id or "system",
185
+ )
186
+ return record
187
+
188
+ # ---- Mutation ------------------------------------------------- #
189
+
190
+ def add_event(
191
+ self,
192
+ event: str,
193
+ actor: Optional[str] = None,
194
+ source_memories: Optional[List[str]] = None,
195
+ outcome: Optional[bool] = None,
196
+ notes: Optional[str] = None,
197
+ **extra: Any,
198
+ ) -> "ProvenanceRecord":
199
+ """Append a new lineage event and bump the version counter."""
200
+ evt = LineageEvent(
201
+ event=event,
202
+ timestamp=datetime.now(timezone.utc).isoformat(),
203
+ actor=actor,
204
+ source_memories=source_memories or [],
205
+ outcome=outcome,
206
+ notes=notes,
207
+ extra=extra,
208
+ )
209
+ self.lineage.append(evt)
210
+ self.version += 1
211
+ return self
212
+
213
+ def mark_consolidated(
214
+ self,
215
+ source_memory_ids: List[str],
216
+ actor: str = "consolidation_worker",
217
+ ) -> "ProvenanceRecord":
218
+ """Convenience wrapper for consolidation events."""
219
+ return self.add_event(
220
+ event="consolidated",
221
+ actor=actor,
222
+ source_memories=source_memory_ids,
223
+ )
224
+
225
+ def mark_verified(
226
+ self,
227
+ success: bool,
228
+ actor: str = "system",
229
+ notes: Optional[str] = None,
230
+ ) -> "ProvenanceRecord":
231
+ """Record a verification outcome."""
232
+ return self.add_event(
233
+ event="verified",
234
+ actor=actor,
235
+ outcome=success,
236
+ notes=notes,
237
+ )
238
+
239
+ def mark_contradicted(
240
+ self,
241
+ contradiction_group_id: str,
242
+ actor: str = "contradiction_detector",
243
+ ) -> "ProvenanceRecord":
244
+ """Flag this memory as contradicted."""
245
+ return self.add_event(
246
+ event="contradicted",
247
+ actor=actor,
248
+ contradiction_group_id=contradiction_group_id,
249
+ )
250
+
251
+ # ---- Serialization -------------------------------------------- #
252
+
253
+ def to_dict(self) -> Dict[str, Any]:
254
+ return {
255
+ "origin": self.origin.to_dict(),
256
+ "lineage": [e.to_dict() for e in self.lineage],
257
+ "version": self.version,
258
+ "confidence_source": self.confidence_source,
259
+ }
260
+
261
+ @classmethod
262
+ def from_dict(cls, d: Dict[str, Any]) -> "ProvenanceRecord":
263
+ return cls(
264
+ origin=ProvenanceOrigin.from_dict(d.get("origin", {"type": "observation"})),
265
+ lineage=[LineageEvent.from_dict(e) for e in d.get("lineage", [])],
266
+ version=d.get("version", 1),
267
+ confidence_source=d.get("confidence_source", "bayesian_ltp"),
268
+ )
269
+
270
+ # ---- Helpers -------------------------------------------------- #
271
+
272
+ @property
273
+ def created_at(self) -> str:
274
+ """ISO timestamp of the creation event."""
275
+ for event in self.lineage:
276
+ if event.event == "created":
277
+ return event.timestamp
278
+ return self.origin.timestamp
279
+
280
+ @property
281
+ def last_event(self) -> Optional[LineageEvent]:
282
+ """Most recent lineage event."""
283
+ return self.lineage[-1] if self.lineage else None
284
+
285
+ def is_contradicted(self) -> bool:
286
+ return any(e.event == "contradicted" for e in self.lineage)
287
+
288
+ def is_verified(self) -> bool:
289
+ return any(
290
+ e.event == "verified" and e.outcome is True for e in self.lineage
291
+ )
292
+
293
+ def __repr__(self) -> str:
294
+ return (
295
+ f"ProvenanceRecord(origin_type={self.origin.type!r}, "
296
+ f"version={self.version}, events={len(self.lineage)})"
297
+ )
src/mnemocore/core/pulse.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Pulse Heartbeat Loop
3
+ ====================
4
+ The central background orchestrator that binds together the AGI cognitive cycles.
5
+ Triggers working memory maintenance, episodic sequence linking, gap tracking, and subconscious inferences.
6
+ """
7
+
8
+ from typing import Optional
9
+ from enum import Enum
10
+ import threading
11
+ import asyncio
12
+ import logging
13
+ import traceback
14
+ from datetime import datetime
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class PulseTick(Enum):
20
+ WM_MAINTENANCE = "wm_maintenance"
21
+ EPISODIC_CHAINING = "episodic_chaining"
22
+ SEMANTIC_REFRESH = "semantic_refresh"
23
+ GAP_DETECTION = "gap_detection"
24
+ INSIGHT_GENERATION = "insight_generation"
25
+ PROCEDURE_REFINEMENT = "procedure_refinement"
26
+ META_SELF_REFLECTION = "meta_self_reflection"
27
+
28
+
29
+ class PulseLoop:
30
+ def __init__(self, container, config):
31
+ """
32
+ Args:
33
+ container: The fully built DI Container containing all memory sub-services.
34
+ config: Specifically the `config.pulse` section settings.
35
+ """
36
+ self.container = container
37
+ self.config = config
38
+ self._running = False
39
+ self._task: Optional[asyncio.Task] = None
40
+
41
+ async def start(self) -> None:
42
+ """Begin the background pulse orchestrator."""
43
+ if not getattr(self.config, "enabled", False):
44
+ logger.info("Pulse loop is disabled via configuration.")
45
+ return
46
+
47
+ self._running = True
48
+ interval = getattr(self.config, "interval_seconds", 30)
49
+ logger.info(f"Starting AGI Pulse Loop (interval={interval}s).")
50
+
51
+ while self._running:
52
+ start_time = datetime.utcnow()
53
+ try:
54
+ await self.tick()
55
+ except asyncio.CancelledError:
56
+ break
57
+ except Exception as e:
58
+ logger.error(f"Error during Pulse tick: {e}", exc_info=True)
59
+
60
+ elapsed = (datetime.utcnow() - start_time).total_seconds()
61
+ sleep_time = max(0.1, interval - elapsed)
62
+ await asyncio.sleep(sleep_time)
63
+
64
+ def stop(self) -> None:
65
+ """Gracefully interrupt and unbind the Pulse loop."""
66
+ self._running = False
67
+ if self._task and not self._task.done():
68
+ self._task.cancel()
69
+ logger.info("AGI Pulse Loop stopped.")
70
+
71
+ async def tick(self) -> None:
72
+ """Execute a full iteration across the cognitive architecture planes."""
73
+ await self._wm_maintenance()
74
+ await self._episodic_chaining()
75
+ await self._semantic_refresh()
76
+ await self._gap_detection()
77
+ await self._insight_generation()
78
+ await self._procedure_refinement()
79
+ await self._meta_self_reflection()
80
+
81
+ async def _wm_maintenance(self) -> None:
82
+ """Prune overloaded short-term buffers and cull expired items."""
83
+ if hasattr(self.container, "working_memory") and self.container.working_memory:
84
+ self.container.working_memory.prune_all()
85
+ logger.debug(f"Pulse: [{PulseTick.WM_MAINTENANCE.value}] Executed.")
86
+
87
+ async def _episodic_chaining(self) -> None:
88
+ """Retroactively verify event streams and apply temporal links between episodic contexts."""
89
+ logger.debug(f"Pulse: [{PulseTick.EPISODIC_CHAINING.value}] Stubbed.")
90
+
91
+ async def _semantic_refresh(self) -> None:
92
+ """Prompt Qdrant abstractions or run `semantic_consolidation` loops over episodic data."""
93
+ logger.debug(f"Pulse: [{PulseTick.SEMANTIC_REFRESH.value}] Stubbed.")
94
+
95
+ async def _gap_detection(self) -> None:
96
+ """Unearth missing knowledge vectors (GapDetector integration)."""
97
+ logger.debug(f"Pulse: [{PulseTick.GAP_DETECTION.value}] Stubbed.")
98
+
99
+ async def _insight_generation(self) -> None:
100
+ """Forward memory patterns to LLM for spontaneous inference generation."""
101
+ logger.debug(f"Pulse: [{PulseTick.INSIGHT_GENERATION.value}] Stubbed.")
102
+
103
+ async def _procedure_refinement(self) -> None:
104
+ """Modify procedure reliabilities directly depending on episode occurrences."""
105
+ logger.debug(f"Pulse: [{PulseTick.PROCEDURE_REFINEMENT.value}] Stubbed.")
106
+
107
+ async def _meta_self_reflection(self) -> None:
108
+ """Collate macro anomalies and submit SelfImprovementProposals."""
109
+ logger.debug(f"Pulse: [{PulseTick.META_SELF_REFLECTION.value}] Stubbed.")
110
+
src/mnemocore/core/qdrant_store.py CHANGED
@@ -6,7 +6,7 @@ Provides async access to Qdrant for vector storage and similarity search.
6
  Phase 4.3: Temporal Recall - supports time-based filtering and indexing.
7
  """
8
 
9
- from typing import List, Any, Optional, Tuple
10
  from datetime import datetime
11
  import asyncio
12
 
@@ -100,7 +100,7 @@ class QdrantStore:
100
  collection_name=self.collection_hot,
101
  vectors_config=models.VectorParams(
102
  size=self.dim,
103
- distance=models.Distance.COSINE,
104
  on_disk=False
105
  ),
106
  quantization_config=quantization_config,
@@ -118,7 +118,7 @@ class QdrantStore:
118
  collection_name=self.collection_warm,
119
  vectors_config=models.VectorParams(
120
  size=self.dim,
121
- distance=models.Distance.MANHATTAN,
122
  on_disk=True
123
  ),
124
  quantization_config=quantization_config,
@@ -169,6 +169,7 @@ class QdrantStore:
169
  limit: int = 5,
170
  score_threshold: float = 0.0,
171
  time_range: Optional[Tuple[datetime, datetime]] = None,
 
172
  ) -> List[models.ScoredPoint]:
173
  """
174
  Async semantic search.
@@ -189,30 +190,51 @@ class QdrantStore:
189
  as search failures should not crash the calling code.
190
  """
191
  try:
192
- # Build time filter if provided (Phase 4.3)
193
- query_filter = None
194
  if time_range:
195
  start_ts = int(time_range[0].timestamp())
196
  end_ts = int(time_range[1].timestamp())
197
- query_filter = models.Filter(
198
- must=[
199
- models.FieldCondition(
200
- key="unix_timestamp",
201
- range=models.Range(
202
- gte=start_ts,
203
- lte=end_ts,
204
- ),
205
  ),
206
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  )
208
 
209
  return await qdrant_breaker.call(
210
- self.client.search,
211
  collection_name=collection,
212
- query_vector=query_vector,
213
  limit=limit,
214
  score_threshold=score_threshold,
215
  query_filter=query_filter,
 
216
  )
217
  except CircuitOpenError:
218
  logger.warning(f"Qdrant search blocked for {collection}: circuit breaker open")
 
6
  Phase 4.3: Temporal Recall - supports time-based filtering and indexing.
7
  """
8
 
9
+ from typing import List, Any, Optional, Tuple, Dict
10
  from datetime import datetime
11
  import asyncio
12
 
 
100
  collection_name=self.collection_hot,
101
  vectors_config=models.VectorParams(
102
  size=self.dim,
103
+ distance=models.Distance.DOT,
104
  on_disk=False
105
  ),
106
  quantization_config=quantization_config,
 
118
  collection_name=self.collection_warm,
119
  vectors_config=models.VectorParams(
120
  size=self.dim,
121
+ distance=models.Distance.DOT,
122
  on_disk=True
123
  ),
124
  quantization_config=quantization_config,
 
169
  limit: int = 5,
170
  score_threshold: float = 0.0,
171
  time_range: Optional[Tuple[datetime, datetime]] = None,
172
+ metadata_filter: Optional[Dict[str, Any]] = None,
173
  ) -> List[models.ScoredPoint]:
174
  """
175
  Async semantic search.
 
190
  as search failures should not crash the calling code.
191
  """
192
  try:
193
+ must_conditions = []
 
194
  if time_range:
195
  start_ts = int(time_range[0].timestamp())
196
  end_ts = int(time_range[1].timestamp())
197
+ must_conditions.append(
198
+ models.FieldCondition(
199
+ key="unix_timestamp",
200
+ range=models.Range(
201
+ gte=start_ts,
202
+ lte=end_ts,
 
 
203
  ),
204
+ )
205
+ )
206
+
207
+ if metadata_filter:
208
+ for k, v in metadata_filter.items():
209
+ must_conditions.append(
210
+ models.FieldCondition(
211
+ key=k,
212
+ match=models.MatchValue(value=v)
213
+ )
214
+ )
215
+
216
+ if must_conditions:
217
+ query_filter = models.Filter(must=must_conditions)
218
+
219
+ # Support for Binary Quantization rescoring (BUG-04)
220
+ search_params = None
221
+ if self.binary_quantization:
222
+ search_params = models.SearchParams(
223
+ quantization=models.QuantizationSearchParams(
224
+ ignore=False,
225
+ rescore=True,
226
+ oversampling=2.0
227
+ )
228
  )
229
 
230
  return await qdrant_breaker.call(
231
+ self.client.query_points,
232
  collection_name=collection,
233
+ query=query_vector,
234
  limit=limit,
235
  score_threshold=score_threshold,
236
  query_filter=query_filter,
237
+ search_params=search_params,
238
  )
239
  except CircuitOpenError:
240
  logger.warning(f"Qdrant search blocked for {collection}: circuit breaker open")
src/mnemocore/core/semantic_consolidation.py CHANGED
@@ -37,6 +37,7 @@ from loguru import logger
37
  from .binary_hdv import BinaryHDV, majority_bundle
38
  from .config import get_config
39
  from .node import MemoryNode
 
40
 
41
 
42
  # ------------------------------------------------------------------ #
@@ -266,6 +267,18 @@ class SemanticConsolidationWorker:
266
  medoid_node.metadata["proto_updated_at"] = datetime.now(timezone.utc).isoformat()
267
  proto_count += 1
268
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  elapsed = time.monotonic() - t0
270
  self.last_run = datetime.now(timezone.utc)
271
  self.stats = {
 
37
  from .binary_hdv import BinaryHDV, majority_bundle
38
  from .config import get_config
39
  from .node import MemoryNode
40
+ from .provenance import ProvenanceRecord
41
 
42
 
43
  # ------------------------------------------------------------------ #
 
267
  medoid_node.metadata["proto_updated_at"] = datetime.now(timezone.utc).isoformat()
268
  proto_count += 1
269
 
270
+ # Phase 5.0: record consolidation in provenance lineage
271
+ source_ids = [n.id for n in member_nodes if n.id != medoid_node.id]
272
+ if medoid_node.provenance is None:
273
+ medoid_node.provenance = ProvenanceRecord.new(
274
+ origin_type="consolidation",
275
+ actor="consolidation_worker",
276
+ )
277
+ medoid_node.provenance.mark_consolidated(
278
+ source_memory_ids=source_ids,
279
+ actor="consolidation_worker",
280
+ )
281
+
282
  elapsed = time.monotonic() - t0
283
  self.last_run = datetime.now(timezone.utc)
284
  self.stats = {