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.
- {memor_cli-0.2.0/memor_cli.egg-info → memor_cli-0.3.0}/PKG-INFO +43 -16
- {memor_cli-0.2.0 → memor_cli-0.3.0}/README.md +42 -15
- memor_cli-0.3.0/memor/__init__.py +1 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/dashboard/static/index.html +4 -4
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/runner.py +3 -2
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/feedback.py +31 -26
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/hook_server.py +48 -1
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/recall.py +35 -3
- memor_cli-0.3.0/memor/retrieve/retriever.py +122 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/store/sqlite_store.py +58 -4
- {memor_cli-0.2.0 → memor_cli-0.3.0/memor_cli.egg-info}/PKG-INFO +43 -16
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/SOURCES.txt +1 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/pyproject.toml +1 -1
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_dashboard.py +3 -3
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_eval_ablation.py +5 -1
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_feedback.py +14 -0
- memor_cli-0.3.0/tests/test_hook_server.py +94 -0
- memor_cli-0.3.0/tests/test_hybrid_retrieval.py +213 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_recall_core.py +63 -3
- memor_cli-0.2.0/memor/__init__.py +0 -1
- memor_cli-0.2.0/memor/retrieve/retriever.py +0 -78
- memor_cli-0.2.0/tests/test_hook_server.py +0 -42
- {memor_cli-0.2.0 → memor_cli-0.3.0}/LICENSE +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/cli.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/daemon.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/dashboard/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/dashboard/server.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/distill/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/distill/distiller.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/distill/extractive.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/api.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/fake.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/embed/local.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/base.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/claude_mem.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/baselines/graphiti.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/dataset.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/embed_benchmark.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/judge.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/eval/metrics.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/hook_cli.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/ingest/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/ingest/claude_code.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/ingest/documents.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/interfaces.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/anthropic.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/base.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/llm/openai_compat.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/project.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/redact.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/retrieve/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/service.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/store/__init__.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/tokencount.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor/types.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/dependency_links.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/entry_points.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/requires.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/memor_cli.egg-info/top_level.txt +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/setup.cfg +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_cli_smoke.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_daemon.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_dataset_builder.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_dimension_safety.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_distiller.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_embed.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_embed_benchmark.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_eval_runner.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_external_baselines.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_extractive.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_hook.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_ingest_claude_code.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_ingest_documents.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_install_hook.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_interfaces.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_judge.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_metrics.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_noise_filter.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_project_resolver.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_redact.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_retriever.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_service.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_skill_recall.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_store.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_supersession.py +0 -0
- {memor_cli-0.2.0 → memor_cli-0.3.0}/tests/test_tokencount.py +0 -0
- {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.
|
|
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)
|
|
49
|
-
[]()
|
|
50
50
|
[]()
|
|
51
51
|
[](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
|
|
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
|
|
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
|
|
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
|
-
|
|
136
|
+
Surviving candidates are ranked by four signals:
|
|
117
137
|
|
|
118
138
|
| Signal | Weight | How it works |
|
|
119
139
|
|---|---|---|
|
|
120
|
-
| **Semantic similarity** | 50% |
|
|
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
|
-
|
|
151
|
-
- **
|
|
152
|
-
- **
|
|
153
|
-
- **
|
|
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
|
|
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
|
|
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 #
|
|
293
|
+
pytest # 179 tests
|
|
267
294
|
```
|
|
268
295
|
|
|
269
296
|
---
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[]()
|
|
13
13
|
[]()
|
|
14
14
|
[](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
|
|
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
|
|
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
|
|
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
|
-
|
|
99
|
+
Surviving candidates are ranked by four signals:
|
|
80
100
|
|
|
81
101
|
| Signal | Weight | How it works |
|
|
82
102
|
|---|---|---|
|
|
83
|
-
| **Semantic similarity** | 50% |
|
|
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
|
-
|
|
114
|
-
- **
|
|
115
|
-
- **
|
|
116
|
-
- **
|
|
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
|
|
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
|
|
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 #
|
|
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
|
|
414
|
-
<div class="side-stat-value" id="e-
|
|
415
|
-
<div class="side-stat-sub">% of prompts
|
|
413
|
+
<div class="side-stat-label">Recall Coverage</div>
|
|
414
|
+
<div class="side-stat-value" id="e-coverage">–</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-
|
|
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,
|
|
76
|
-
|
|
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
|
-
|
|
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) <
|
|
42
|
+
if len(words) < _MIN_WORDS:
|
|
40
43
|
return False
|
|
41
|
-
|
|
42
|
-
for i in range(
|
|
43
|
-
|
|
44
|
-
if not
|
|
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
|
|
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(
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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) <=
|
|
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:
|