memor-cli 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {memor_cli-0.2.0/memor_cli.egg-info → memor_cli-0.3.0}/PKG-INFO +43 -16
  2. {memor_cli-0.2.0 → memor_cli-0.3.0}/README.md +42 -15
  3. memor_cli-0.3.0/memor/__init__.py +1 -0
  4. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/dashboard/static/index.html +4 -4
  5. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/runner.py +3 -2
  6. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/feedback.py +31 -26
  7. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/hook_server.py +48 -1
  8. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/recall.py +35 -3
  9. memor_cli-0.3.0/memor/retrieve/retriever.py +122 -0
  10. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/store/sqlite_store.py +58 -4
  11. {memor_cli-0.2.0 → memor_cli-0.3.0/memor_cli.egg-info}/PKG-INFO +43 -16
  12. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/SOURCES.txt +1 -0
  13. {memor_cli-0.2.0 → memor_cli-0.3.0}/pyproject.toml +1 -1
  14. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_dashboard.py +3 -3
  15. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_eval_ablation.py +5 -1
  16. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_feedback.py +14 -0
  17. memor_cli-0.3.0/tests/test_hook_server.py +94 -0
  18. memor_cli-0.3.0/tests/test_hybrid_retrieval.py +213 -0
  19. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_recall_core.py +63 -3
  20. memor_cli-0.2.0/memor/__init__.py +0 -1
  21. memor_cli-0.2.0/memor/retrieve/retriever.py +0 -78
  22. memor_cli-0.2.0/tests/test_hook_server.py +0 -42
  23. {memor_cli-0.2.0 → memor_cli-0.3.0}/LICENSE +0 -0
  24. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/cli.py +0 -0
  25. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/daemon.py +0 -0
  26. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/dashboard/__init__.py +0 -0
  27. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/dashboard/server.py +0 -0
  28. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/distill/__init__.py +0 -0
  29. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/distill/distiller.py +0 -0
  30. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/distill/extractive.py +0 -0
  31. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/__init__.py +0 -0
  32. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/api.py +0 -0
  33. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/fake.py +0 -0
  34. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/local.py +0 -0
  35. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/__init__.py +0 -0
  36. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/__init__.py +0 -0
  37. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/base.py +0 -0
  38. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/claude_mem.py +0 -0
  39. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/graphiti.py +0 -0
  40. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/dataset.py +0 -0
  41. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/embed_benchmark.py +0 -0
  42. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/judge.py +0 -0
  43. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/metrics.py +0 -0
  44. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/hook_cli.py +0 -0
  45. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/ingest/__init__.py +0 -0
  46. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/ingest/claude_code.py +0 -0
  47. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/ingest/documents.py +0 -0
  48. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/interfaces.py +0 -0
  49. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/__init__.py +0 -0
  50. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/anthropic.py +0 -0
  51. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/base.py +0 -0
  52. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/openai_compat.py +0 -0
  53. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/project.py +0 -0
  54. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/redact.py +0 -0
  55. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/retrieve/__init__.py +0 -0
  56. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/service.py +0 -0
  57. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/store/__init__.py +0 -0
  58. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/tokencount.py +0 -0
  59. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/types.py +0 -0
  60. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/dependency_links.txt +0 -0
  61. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/entry_points.txt +0 -0
  62. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/requires.txt +0 -0
  63. {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/top_level.txt +0 -0
  64. {memor_cli-0.2.0 → memor_cli-0.3.0}/setup.cfg +0 -0
  65. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_cli_smoke.py +0 -0
  66. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_daemon.py +0 -0
  67. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_dataset_builder.py +0 -0
  68. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_dimension_safety.py +0 -0
  69. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_distiller.py +0 -0
  70. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_embed.py +0 -0
  71. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_embed_benchmark.py +0 -0
  72. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_eval_runner.py +0 -0
  73. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_external_baselines.py +0 -0
  74. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_extractive.py +0 -0
  75. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_hook.py +0 -0
  76. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_ingest_claude_code.py +0 -0
  77. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_ingest_documents.py +0 -0
  78. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_install_hook.py +0 -0
  79. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_interfaces.py +0 -0
  80. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_judge.py +0 -0
  81. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_metrics.py +0 -0
  82. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_noise_filter.py +0 -0
  83. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_project_resolver.py +0 -0
  84. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_redact.py +0 -0
  85. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_retriever.py +0 -0
  86. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_service.py +0 -0
  87. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_skill_recall.py +0 -0
  88. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_store.py +0 -0
  89. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_supersession.py +0 -0
  90. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_tokencount.py +0 -0
  91. {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memor-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Measured memory for coding agents. Fire and forget — no API keys needed.
5
5
  Author-email: Nimit Bhandari <nimitbhandari17@gmail.com>
6
6
  License-Expression: MIT
@@ -46,7 +46,7 @@ Dynamic: license-file
46
46
  ```
47
47
 
48
48
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
49
- [![Tests](https://img.shields.io/badge/tests-153%20passing-brightgreen.svg)]()
49
+ [![Tests](https://img.shields.io/badge/tests-179%20passing-brightgreen.svg)]()
50
50
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)]()
51
51
  [![PyPI](https://img.shields.io/pypi/v/memor-cli.svg)](https://pypi.org/project/memor-cli/)
52
52
 
@@ -65,7 +65,10 @@ pipx install memor-cli
65
65
  # Install the Claude Code hook + download embedding model (~60MB)
66
66
  memor install-hook
67
67
 
68
- # Start the background daemon
68
+ # Start as a background service (macOS/Linux)
69
+ memor service install
70
+
71
+ # Or run in the foreground
69
72
  memor daemon
70
73
  ```
71
74
 
@@ -92,7 +95,13 @@ memor dashboard
92
95
  Embed query locally (model2vec, ~2ms)
93
96
  |
94
97
  v
95
- Hybrid scoring: similarity + recency + kind weight + quality
98
+ Hybrid retrieval: dense vectors + lexical BM25, fused (RRF)
99
+ |
100
+ v
101
+ Relevance gate drops off-topic matches (inject nothing if nothing fits)
102
+ |
103
+ v
104
+ Rank: similarity + recency + kind weight + quality
96
105
  |
97
106
  v
98
107
  Inject relevant context into prompt
@@ -104,20 +113,31 @@ memor dashboard
104
113
 
105
114
  **Two background processes:**
106
115
 
107
- 1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates. All local.
116
+ 1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates, tracks session-level token usage. All local.
108
117
  2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms.
109
118
 
110
119
  **No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
111
120
 
112
121
  ---
113
122
 
114
- ## Hybrid Scoring
123
+ ## Hybrid Retrieval
124
+
125
+ Memor retrieves over two channels and fuses them, so it catches both semantic matches and exact terms:
126
+
127
+ - **Dense** — local vector similarity (model2vec) for semantic recall.
128
+ - **Lexical** — SQLite FTS5 / BM25 over the raw text, to recover exact identifiers, error strings, and API names that static embeddings blur together.
129
+
130
+ The two rankings are combined with **Reciprocal Rank Fusion (RRF)**. A **relevance gate** drops anti-correlated (off-topic) candidates *before* ranking, so an unrelated prompt injects nothing rather than the least-bad guess. The lexical channel only activates when the dense channel finds the query on-topic, preventing generic words from pulling in noise.
131
+
132
+ > Tunable via `MEMOR_MIN_SIMILARITY` (relevance floor, default 0.0) and `MEMOR_MAX_TOKENS` (injection budget, default 1500).
133
+
134
+ ## Scoring
115
135
 
116
- Memor doesn't just match keywords. Each memory is scored by four signals:
136
+ Surviving candidates are ranked by four signals:
117
137
 
118
138
  | Signal | Weight | How it works |
119
139
  |---|---|---|
120
- | **Semantic similarity** | 50% | Vector cosine distance between query and memory |
140
+ | **Semantic similarity** | 50% | Dense + lexical relevance, fused via RRF |
121
141
  | **Recency** | 25% | Exponential decay with 14-day half-life — recent decisions rank higher |
122
142
  | **Kind weight** | 15% | Distilled memories (1.3x) rank above raw session chunks (1.0x) |
123
143
  | **Quality** | 10% | Bayesian score from implicit feedback — memories the agent actually uses rank higher |
@@ -147,10 +167,11 @@ Memories are automatically classified as `decision`, `bugfix`, `lesson`, `snippe
147
167
  memor dashboard
148
168
  ```
149
169
 
150
- Shows:
151
- - **Memory bank** — session chunks, distilled memories, projects tracked
152
- - **Context efficiency** — overhead %, recall precision, quality scores per session
153
- - **Per-project breakdown** — which projects have the most context
170
+ Dark fintech-inspired UI showing:
171
+ - **Hero metrics** — total memories, recall count, avg latency, coverage — with sparkline bars
172
+ - **Daily recall activity** — stacked bar chart of hits vs misses over time
173
+ - **Session efficiency** — real token savings measured from API usage data (avg tokens/turn with vs without recall)
174
+ - **Per-project breakdown** — artifact counts, token totals, last activity
154
175
  - **Recent recalls** — every hook event with scores, latency, and status
155
176
 
156
177
  ---
@@ -162,6 +183,11 @@ memor help Print the full manual
162
183
  memor install-hook Install Claude Code hook + download model
163
184
  memor daemon Auto-ingest + distill (background watcher)
164
185
  memor dashboard Web dashboard on localhost:8420
186
+ memor version Print installed version
187
+ memor service install Run daemon as background service (launchd/systemd)
188
+ memor service stop Stop the background service
189
+ memor service uninstall Remove the background service
190
+ memor service status Check if the service is running
165
191
  memor query <text> Search memories from the CLI
166
192
  memor reingest Wipe DB and re-ingest everything
167
193
  memor reingest --project <name> Re-ingest only one project
@@ -193,16 +219,17 @@ memor/
193
219
  +-- feedback.py Implicit feedback analyzer (usage detection)
194
220
  |
195
221
  +-- retrieve/
196
- | +-- retriever.py Hybrid scoring: similarity + recency + kind + quality
222
+ | +-- retriever.py Hybrid retrieval (dense + BM25, RRF) + relevance gate + scoring
197
223
  |
198
224
  +-- store/
199
- | +-- sqlite_store.py SQLite + sqlite-vec (WAL mode, dimension safety)
225
+ | +-- sqlite_store.py SQLite + sqlite-vec + FTS5 (WAL mode, dimension safety)
200
226
  |
201
227
  +-- embed/
202
228
  | +-- local.py model2vec (potion-base-8M, 256-dim, ~60MB)
203
229
  | +-- api.py OpenAI-compatible embedding API (optional)
204
230
  | +-- fake.py Deterministic SHA-256 embedder (tests)
205
231
  |
232
+ +-- service.py Background service management (launchd/systemd)
206
233
  +-- dashboard/
207
234
  | +-- server.py FastAPI dashboard backend
208
235
  | +-- static/index.html Self-contained dashboard (no CDN deps)
@@ -227,7 +254,7 @@ skill/recall.py Standalone recall script
227
254
  **Nothing leaves your machine.** In the default configuration:
228
255
 
229
256
  - **No telemetry, no analytics, no phone-home.** Zero outbound network calls.
230
- - **Embeddings run locally** via model2vec ONNX (one-time model download from HuggingFace — no user data sent).
257
+ - **Embeddings run locally** via model2vec static token embeddings — no inference runtime, no GPU (one-time model download from HuggingFace — no user data sent).
231
258
  - **Hook transport is a Unix socket** (`~/.memor/hook.sock`), not a network port.
232
259
  - **Dashboard binds localhost only.**
233
260
 
@@ -263,7 +290,7 @@ cd memor-ai
263
290
  python3 -m venv .venv && source .venv/bin/activate
264
291
  pip install -e ".[dev]"
265
292
 
266
- pytest # 153 tests
293
+ pytest # 179 tests
267
294
  ```
268
295
 
269
296
  ---
@@ -9,7 +9,7 @@
9
9
  ```
10
10
 
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
12
- [![Tests](https://img.shields.io/badge/tests-153%20passing-brightgreen.svg)]()
12
+ [![Tests](https://img.shields.io/badge/tests-179%20passing-brightgreen.svg)]()
13
13
  [![Python](https://img.shields.io/badge/python-3.11%2B-blue.svg)]()
14
14
  [![PyPI](https://img.shields.io/pypi/v/memor-cli.svg)](https://pypi.org/project/memor-cli/)
15
15
 
@@ -28,7 +28,10 @@ pipx install memor-cli
28
28
  # Install the Claude Code hook + download embedding model (~60MB)
29
29
  memor install-hook
30
30
 
31
- # Start the background daemon
31
+ # Start as a background service (macOS/Linux)
32
+ memor service install
33
+
34
+ # Or run in the foreground
32
35
  memor daemon
33
36
  ```
34
37
 
@@ -55,7 +58,13 @@ memor dashboard
55
58
  Embed query locally (model2vec, ~2ms)
56
59
  |
57
60
  v
58
- Hybrid scoring: similarity + recency + kind weight + quality
61
+ Hybrid retrieval: dense vectors + lexical BM25, fused (RRF)
62
+ |
63
+ v
64
+ Relevance gate drops off-topic matches (inject nothing if nothing fits)
65
+ |
66
+ v
67
+ Rank: similarity + recency + kind weight + quality
59
68
  |
60
69
  v
61
70
  Inject relevant context into prompt
@@ -67,20 +76,31 @@ memor dashboard
67
76
 
68
77
  **Two background processes:**
69
78
 
70
- 1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates. All local.
79
+ 1. **Daemon** — polls `~/.claude/projects/` for transcripts, embeds chunks, runs distillation, analyzes feedback, compacts duplicates, tracks session-level token usage. All local.
71
80
  2. **Hook** — fires on every prompt, recalls relevant memories, injects them as context. Sub-15ms.
72
81
 
73
82
  **No API keys required.** Embeddings run locally via [model2vec](https://github.com/MinishLab/model2vec) (potion-base-8M, 256-dim). Vectors stored in [sqlite-vec](https://github.com/asg017/sqlite-vec). Everything runs on your machine.
74
83
 
75
84
  ---
76
85
 
77
- ## Hybrid Scoring
86
+ ## Hybrid Retrieval
87
+
88
+ Memor retrieves over two channels and fuses them, so it catches both semantic matches and exact terms:
89
+
90
+ - **Dense** — local vector similarity (model2vec) for semantic recall.
91
+ - **Lexical** — SQLite FTS5 / BM25 over the raw text, to recover exact identifiers, error strings, and API names that static embeddings blur together.
92
+
93
+ The two rankings are combined with **Reciprocal Rank Fusion (RRF)**. A **relevance gate** drops anti-correlated (off-topic) candidates *before* ranking, so an unrelated prompt injects nothing rather than the least-bad guess. The lexical channel only activates when the dense channel finds the query on-topic, preventing generic words from pulling in noise.
94
+
95
+ > Tunable via `MEMOR_MIN_SIMILARITY` (relevance floor, default 0.0) and `MEMOR_MAX_TOKENS` (injection budget, default 1500).
96
+
97
+ ## Scoring
78
98
 
79
- Memor doesn't just match keywords. Each memory is scored by four signals:
99
+ Surviving candidates are ranked by four signals:
80
100
 
81
101
  | Signal | Weight | How it works |
82
102
  |---|---|---|
83
- | **Semantic similarity** | 50% | Vector cosine distance between query and memory |
103
+ | **Semantic similarity** | 50% | Dense + lexical relevance, fused via RRF |
84
104
  | **Recency** | 25% | Exponential decay with 14-day half-life — recent decisions rank higher |
85
105
  | **Kind weight** | 15% | Distilled memories (1.3x) rank above raw session chunks (1.0x) |
86
106
  | **Quality** | 10% | Bayesian score from implicit feedback — memories the agent actually uses rank higher |
@@ -110,10 +130,11 @@ Memories are automatically classified as `decision`, `bugfix`, `lesson`, `snippe
110
130
  memor dashboard
111
131
  ```
112
132
 
113
- Shows:
114
- - **Memory bank** — session chunks, distilled memories, projects tracked
115
- - **Context efficiency** — overhead %, recall precision, quality scores per session
116
- - **Per-project breakdown** — which projects have the most context
133
+ Dark fintech-inspired UI showing:
134
+ - **Hero metrics** — total memories, recall count, avg latency, coverage — with sparkline bars
135
+ - **Daily recall activity** — stacked bar chart of hits vs misses over time
136
+ - **Session efficiency** — real token savings measured from API usage data (avg tokens/turn with vs without recall)
137
+ - **Per-project breakdown** — artifact counts, token totals, last activity
117
138
  - **Recent recalls** — every hook event with scores, latency, and status
118
139
 
119
140
  ---
@@ -125,6 +146,11 @@ memor help Print the full manual
125
146
  memor install-hook Install Claude Code hook + download model
126
147
  memor daemon Auto-ingest + distill (background watcher)
127
148
  memor dashboard Web dashboard on localhost:8420
149
+ memor version Print installed version
150
+ memor service install Run daemon as background service (launchd/systemd)
151
+ memor service stop Stop the background service
152
+ memor service uninstall Remove the background service
153
+ memor service status Check if the service is running
128
154
  memor query <text> Search memories from the CLI
129
155
  memor reingest Wipe DB and re-ingest everything
130
156
  memor reingest --project <name> Re-ingest only one project
@@ -156,16 +182,17 @@ memor/
156
182
  +-- feedback.py Implicit feedback analyzer (usage detection)
157
183
  |
158
184
  +-- retrieve/
159
- | +-- retriever.py Hybrid scoring: similarity + recency + kind + quality
185
+ | +-- retriever.py Hybrid retrieval (dense + BM25, RRF) + relevance gate + scoring
160
186
  |
161
187
  +-- store/
162
- | +-- sqlite_store.py SQLite + sqlite-vec (WAL mode, dimension safety)
188
+ | +-- sqlite_store.py SQLite + sqlite-vec + FTS5 (WAL mode, dimension safety)
163
189
  |
164
190
  +-- embed/
165
191
  | +-- local.py model2vec (potion-base-8M, 256-dim, ~60MB)
166
192
  | +-- api.py OpenAI-compatible embedding API (optional)
167
193
  | +-- fake.py Deterministic SHA-256 embedder (tests)
168
194
  |
195
+ +-- service.py Background service management (launchd/systemd)
169
196
  +-- dashboard/
170
197
  | +-- server.py FastAPI dashboard backend
171
198
  | +-- static/index.html Self-contained dashboard (no CDN deps)
@@ -190,7 +217,7 @@ skill/recall.py Standalone recall script
190
217
  **Nothing leaves your machine.** In the default configuration:
191
218
 
192
219
  - **No telemetry, no analytics, no phone-home.** Zero outbound network calls.
193
- - **Embeddings run locally** via model2vec ONNX (one-time model download from HuggingFace — no user data sent).
220
+ - **Embeddings run locally** via model2vec static token embeddings — no inference runtime, no GPU (one-time model download from HuggingFace — no user data sent).
194
221
  - **Hook transport is a Unix socket** (`~/.memor/hook.sock`), not a network port.
195
222
  - **Dashboard binds localhost only.**
196
223
 
@@ -226,7 +253,7 @@ cd memor-ai
226
253
  python3 -m venv .venv && source .venv/bin/activate
227
254
  pip install -e ".[dev]"
228
255
 
229
- pytest # 153 tests
256
+ pytest # 179 tests
230
257
  ```
231
258
 
232
259
  ---
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -410,9 +410,9 @@
410
410
  <div class="side-stat-sub" id="e-avg-input-sub">across all sessions</div>
411
411
  </div>
412
412
  <div class="side-stat">
413
- <div class="side-stat-label">Recall Precision</div>
414
- <div class="side-stat-value" id="e-precision">&ndash;</div>
415
- <div class="side-stat-sub">% of prompts with relevant context</div>
413
+ <div class="side-stat-label">Recall Coverage</div>
414
+ <div class="side-stat-value" id="e-coverage">&ndash;</div>
415
+ <div class="side-stat-sub">% of prompts where context was injected</div>
416
416
  </div>
417
417
  <div class="side-stat">
418
418
  <div class="side-stat-label">Avg Latency</div>
@@ -581,7 +581,7 @@
581
581
 
582
582
  /* ── Efficiency ────────────────────────────────────────── */
583
583
  function renderEfficiency(data) {
584
- document.getElementById('e-precision').textContent = fmtPct(data.precision * 100);
584
+ document.getElementById('e-coverage').textContent = fmtPct(data.coverage * 100);
585
585
  }
586
586
 
587
587
  function renderSessionEfficiency(data) {
@@ -72,7 +72,8 @@ def run_ablation(*, query, project, relevant_ids, store, embedder, k=8):
72
72
  "ndcg@k": ndcg_at_k(ids, relevant_ids, k)}
73
73
  return out
74
74
 
75
- def run_contradiction_eval(*, query, project, stale_id, current_id, store, embedder, k=8):
76
- r = Retriever(store, embedder, k=k, edge_expand=True)
75
+ def run_contradiction_eval(*, query, project, stale_id, current_id, store, embedder,
76
+ k=8, min_similarity=0.0):
77
+ r = Retriever(store, embedder, k=k, edge_expand=True, min_similarity=min_similarity)
77
78
  ids = [h.artifact.id for h in r.query(query, Scope(project=project)).hits]
78
79
  return (current_id in ids) and (stale_id not in ids)
@@ -5,10 +5,13 @@ if the agent's responses referenced recalled content. Updates memory_quality
5
5
  scores accordingly."""
6
6
  from __future__ import annotations
7
7
  import json
8
+ import math
8
9
  from pathlib import Path
9
10
  from memor.store.sqlite_store import SqliteStore
10
11
 
11
- MIN_OVERLAP_TOKENS = 5
12
+ _NGRAM_SIZE = 3
13
+ _MIN_WORDS = 4
14
+ _MATCH_RATIO = 0.10
12
15
 
13
16
 
14
17
  def _extract_assistant_texts(transcript_path: Path) -> list[str]:
@@ -36,44 +39,46 @@ def _extract_assistant_texts(transcript_path: Path) -> list[str]:
36
39
 
37
40
  def _text_was_used(memory_text: str, assistant_texts: list[str]) -> bool:
38
41
  words = memory_text.lower().split()
39
- if len(words) < MIN_OVERLAP_TOKENS:
42
+ if len(words) < _MIN_WORDS:
40
43
  return False
41
- key_phrases = []
42
- for i in range(0, len(words) - 4):
43
- key_phrases.append(" ".join(words[i:i+5]))
44
- if not key_phrases:
44
+ ngrams = []
45
+ for i in range(len(words) - _NGRAM_SIZE + 1):
46
+ ngrams.append(" ".join(words[i:i + _NGRAM_SIZE]))
47
+ if not ngrams:
45
48
  return False
46
49
  matches = 0
47
- for phrase in key_phrases:
50
+ for phrase in ngrams:
48
51
  for text in assistant_texts:
49
52
  if phrase in text:
50
53
  matches += 1
51
54
  break
52
- return matches >= max(2, len(key_phrases) // 5)
55
+ return matches >= max(1, math.ceil(len(ngrams) * _MATCH_RATIO))
53
56
 
54
57
 
55
58
  def analyze_session_feedback(
56
59
  store: SqliteStore, session_id: str, transcript_path: Path
57
60
  ) -> int:
58
- recalls = store.db.execute(
59
- "SELECT * FROM recall_log WHERE session_id=? AND hits_count > 0",
60
- (session_id,)
61
- ).fetchall()
62
- if not recalls:
63
- return 0
64
-
65
61
  recalled_ids = set()
66
- for r in recalls:
67
- log_id = r["id"]
68
- rl_time = r["timestamp"]
69
- nearby = store.db.execute("""
70
- SELECT q.artifact_id FROM memory_quality q
71
- JOIN artifacts a ON a.id = q.artifact_id
72
- WHERE q.last_recalled BETWEEN ? - 2 AND ? + 2
73
- AND a.active = 1
74
- """, (rl_time, rl_time)).fetchall()
75
- for row in nearby:
76
- recalled_ids.add(row["artifact_id"])
62
+ rows = store.db.execute("""
63
+ SELECT q.artifact_id FROM memory_quality q
64
+ JOIN artifacts a ON a.id = q.artifact_id
65
+ WHERE a.active = 1
66
+ AND a.project = (
67
+ SELECT project FROM recall_log
68
+ WHERE session_id = ? AND hits_count > 0
69
+ LIMIT 1
70
+ )
71
+ AND q.last_recalled >= (
72
+ SELECT MIN(timestamp) FROM recall_log
73
+ WHERE session_id = ? AND hits_count > 0
74
+ )
75
+ AND q.last_recalled <= (
76
+ SELECT MAX(timestamp) FROM recall_log
77
+ WHERE session_id = ? AND hits_count > 0
78
+ ) + 5
79
+ """, (session_id, session_id, session_id)).fetchall()
80
+ for row in rows:
81
+ recalled_ids.add(row["artifact_id"])
77
82
 
78
83
  if not recalled_ids:
79
84
  return 0
@@ -11,9 +11,18 @@ SOCK_PATH = Path.home() / ".memor" / "hook.sock"
11
11
  PID_PATH = Path.home() / ".memor" / "hook.pid"
12
12
  DEFAULT_DB = str(Path.home() / ".memor" / "memor.db")
13
13
  IDLE_TIMEOUT_S = 600
14
+ MIN_QUERY_WORDS = 10
15
+ _TRIVIAL_PATTERNS = frozenset({
16
+ "yes", "no", "ok", "okay", "sure", "thanks", "thank you", "ty",
17
+ "looks good", "lgtm", "continue", "go ahead", "do it", "proceed",
18
+ "correct", "right", "yep", "yup", "nope", "agreed", "sounds good",
19
+ "perfect", "great", "nice", "cool", "done", "got it", "k",
20
+ })
14
21
 
15
22
  _embedder = None
16
23
  _last_activity = 0.0
24
+ _session_injected: dict[str, set[str]] = {}
25
+ _MAX_TRACKED_SESSIONS = 50
17
26
 
18
27
  _UNSET = object() # sentinel for "auto-discover embedder"
19
28
 
@@ -53,8 +62,46 @@ def handle_request(req: dict, *, db_path: str = DEFAULT_DB,
53
62
  }
54
63
  }
55
64
 
65
+ query_stripped = query.strip().rstrip("?!.,").strip().lower()
66
+ query_word_count = len(query.split())
67
+ if query_word_count < MIN_QUERY_WORDS and query_stripped in _TRIVIAL_PATTERNS:
68
+ msg = "Memor: skipped — trivial prompt"
69
+ if Path(db_path).exists():
70
+ try:
71
+ from memor.store.sqlite_store import SqliteStore
72
+ store = SqliteStore(db_path, dim=embedder.dim)
73
+ store.log_recall(
74
+ project=project, query_preview=query[:100],
75
+ hits_count=0, top_score=0.0,
76
+ tokens_injected=0, latency_ms=0.0,
77
+ status="skipped_trivial", session_id=session_id)
78
+ except Exception:
79
+ pass
80
+ return {
81
+ "hookSpecificOutput": {
82
+ "hookEventName": "UserPromptSubmit",
83
+ "additionalContext": f"---\n{msg}",
84
+ }
85
+ }
86
+
87
+ try:
88
+ max_tokens = max(0, int(os.environ.get("MEMOR_MAX_TOKENS", "1500")))
89
+ except (ValueError, TypeError):
90
+ max_tokens = 1500
91
+ try:
92
+ min_similarity = float(os.environ.get("MEMOR_MIN_SIMILARITY", "0.0"))
93
+ except (ValueError, TypeError):
94
+ min_similarity = 0.0
95
+ already_injected = _session_injected.get(session_id, set()) if session_id else set()
56
96
  result = recall(query, project, db_path, embedder=embedder, k=8, threshold=0.15,
57
- session_id=session_id)
97
+ max_tokens=max_tokens, min_similarity=min_similarity,
98
+ exclude_ids=already_injected or None, session_id=session_id)
99
+
100
+ if session_id and result.hit_ids:
101
+ _session_injected.setdefault(session_id, set()).update(result.hit_ids)
102
+ if len(_session_injected) > _MAX_TRACKED_SESSIONS:
103
+ oldest = next(iter(_session_injected))
104
+ del _session_injected[oldest]
58
105
 
59
106
  if Path(db_path).exists():
60
107
  try:
@@ -47,11 +47,31 @@ def _status_message(status: str, project: str, hits_count: int,
47
47
  return 'Memor: memory store is empty — run "memor daemon" to start ingesting sessions'
48
48
  if status == "no_embedder":
49
49
  return "Memor: inactive — run 'memor setup-model' to download the embedding model"
50
+ if status == "skipped_trivial":
51
+ return "Memor: skipped — trivial prompt"
50
52
  return f"Memor: status={status}"
51
53
 
52
54
 
55
+ DEFAULT_MAX_TOKENS = 1500
56
+ _TEXT_TRUNCATE_LEN = 600
57
+
58
+
59
+ def _injected_token_count(artifact) -> int:
60
+ """Token cost of the text actually injected (after 600-char truncation)."""
61
+ if len(artifact.text) <= _TEXT_TRUNCATE_LEN:
62
+ return artifact.token_count
63
+ from memor.tokencount import count_tokens
64
+ return max(1, count_tokens(artifact.text[:_TEXT_TRUNCATE_LEN]))
65
+
66
+
67
+ DEFAULT_MIN_SIMILARITY = 0.0
68
+
69
+
53
70
  def recall(query: str, project: str, db_path: str, *,
54
71
  embedder=None, k: int = 8, threshold: float = 0.3,
72
+ max_tokens: int = DEFAULT_MAX_TOKENS,
73
+ min_similarity: float = DEFAULT_MIN_SIMILARITY,
74
+ exclude_ids: set[str] | None = None,
55
75
  session_id: str = "") -> RecallResult:
56
76
  t0 = time.perf_counter()
57
77
 
@@ -67,16 +87,28 @@ def recall(query: str, project: str, db_path: str, *,
67
87
  from memor.retrieve.retriever import Retriever
68
88
 
69
89
  store = SqliteStore(db_path, dim=embedder.dim)
70
- retriever = Retriever(store, embedder, k=k)
90
+ retriever = Retriever(store, embedder, k=k, min_similarity=min_similarity)
71
91
  trace = retriever.query(query, Scope(project=project))
72
92
 
73
93
  hits = list(trace.hits)
74
94
  if session_id:
75
95
  hits = [h for h in hits if h.artifact.meta.get("session_id") != session_id]
96
+ if exclude_ids:
97
+ hits = [h for h in hits if h.artifact.id not in exclude_ids]
76
98
  if threshold > 0.0:
77
99
  hits = [h for h in hits if h.score >= threshold]
100
+ if max_tokens > 0:
101
+ budget_hits = []
102
+ running = 0
103
+ for h in hits:
104
+ cost = _injected_token_count(h.artifact)
105
+ if running + cost > max_tokens and budget_hits:
106
+ break
107
+ budget_hits.append(h)
108
+ running += cost
109
+ hits = budget_hits
78
110
  top_score = hits[0].score if hits else 0.0
79
- tokens = sum(h.artifact.token_count for h in hits)
111
+ tokens = sum(_injected_token_count(h.artifact) for h in hits)
80
112
 
81
113
  if not hits:
82
114
  status = "no_hits"
@@ -92,7 +124,7 @@ def recall(query: str, project: str, db_path: str, *,
92
124
  for i, h in enumerate(hits, 1):
93
125
  a = h.artifact
94
126
  kind_tag = a.meta.get("mem_type", a.kind)
95
- text = a.text if len(a.text) <= 600 else a.text[:600] + "..."
127
+ text = a.text if len(a.text) <= _TEXT_TRUNCATE_LEN else a.text[:_TEXT_TRUNCATE_LEN] + "..."
96
128
  source_parts = []
97
129
  sid = a.meta.get("session_id")
98
130
  if sid: