voltmem 0.2.0__tar.gz → 0.2.1__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 (28) hide show
  1. {voltmem-0.2.0 → voltmem-0.2.1}/PKG-INFO +61 -4
  2. {voltmem-0.2.0 → voltmem-0.2.1}/README.md +60 -3
  3. {voltmem-0.2.0 → voltmem-0.2.1}/pyproject.toml +1 -1
  4. voltmem-0.2.1/tests/test_classifiers.py +128 -0
  5. voltmem-0.2.1/tests/test_discovery.py +142 -0
  6. {voltmem-0.2.0 → voltmem-0.2.1}/tests/test_voltmem.py +153 -0
  7. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/__init__.py +26 -1
  8. voltmem-0.2.1/voltmem/classifiers.py +168 -0
  9. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/client.py +88 -10
  10. voltmem-0.2.1/voltmem/discovery.py +155 -0
  11. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/domains.py +62 -1
  12. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/extract.py +12 -3
  13. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/memory.py +67 -13
  14. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/scoring.py +83 -5
  15. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/store.py +55 -0
  16. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/PKG-INFO +61 -4
  17. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/SOURCES.txt +4 -0
  18. {voltmem-0.2.0 → voltmem-0.2.1}/LICENSE +0 -0
  19. {voltmem-0.2.0 → voltmem-0.2.1}/setup.cfg +0 -0
  20. {voltmem-0.2.0 → voltmem-0.2.1}/tests/test_client.py +0 -0
  21. {voltmem-0.2.0 → voltmem-0.2.1}/tests/test_vector_index.py +0 -0
  22. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/embeddings.py +0 -0
  23. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/integrations/__init__.py +0 -0
  24. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/integrations/langchain.py +0 -0
  25. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem/vector_index.py +0 -0
  26. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/dependency_links.txt +0 -0
  27. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/requires.txt +0 -0
  28. {voltmem-0.2.0 → voltmem-0.2.1}/voltmem.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voltmem
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Current-truth memory for LLM agents — protect stable facts, update volatile ones.
5
5
  Author: VoltMem contributors
6
6
  License-Expression: MIT
@@ -31,7 +31,7 @@ Dynamic: license-file
31
31
 
32
32
  # VoltMem
33
33
 
34
- [![PyPI](https://img.shields.io/pypi/v/voltmem)](https://pypi.org/project/voltmem/)
34
+ [![Version](https://img.shields.io/badge/version-0.2.1-blue)](https://pypi.org/project/voltmem/)
35
35
  [![Python](https://img.shields.io/pypi/pyversions/voltmem)](https://pypi.org/project/voltmem/)
36
36
  [![License: MIT](https://img.shields.io/pypi/l/voltmem)](https://github.com/Rouche01/voltmem/blob/main/LICENSE)
37
37
 
@@ -47,7 +47,7 @@ volatile memories rank lower at search time.
47
47
 
48
48
  > Mem0 remembers relevant facts. VoltMem remembers **current truth**.
49
49
 
50
- **Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md)
50
+ **Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md) · **Known limits & roadmap:** [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md)
51
51
 
52
52
  ---
53
53
 
@@ -133,6 +133,7 @@ flowchart LR
133
133
  | User moves cities | Berlin and Paris both stored | **Updates** to current city |
134
134
  | Old project name in haystack | Ranks by similarity | **Down-ranks** stale volatile facts |
135
135
  | Confident wrong blip on stable pref | Often accepted | **Resists** corruption |
136
+ | Career / role change (medium-stable) | Often blocked or duplicated | **Updates** on strong explicit contradiction |
136
137
 
137
138
  ### Example results (reproducible)
138
139
 
@@ -168,6 +169,15 @@ Run locally with `pip install -e ".[embeddings]"`. Embeddings:
168
169
  **VoltMem clearer wins: 3/3.** Mem0 keeps contradictory facts; VoltMem updates volatile
169
170
  slots and protects stable prefs via domain volatility + slot-aware linking.
170
171
 
172
+ **`experiments/voltmem_eval.py`** — end-to-end escalation + retrieval (real vs flat vs swap):
173
+
174
+ | Battery | real profile | flat (equal V) | swap (inverted V) |
175
+ |---|---|---|---|
176
+ | A — selective updating | **20/20** | 15/20 | 7/20 |
177
+ | B — retrieval separation | **+0.589** | +0.202 | −0.267 |
178
+
179
+ Includes the `professional_context` career-change probe (strong explicit evidence → update).
180
+
171
181
  **`experiments/memory_demo.py`** — 3 final Q&A checks vs ground truth:
172
182
 
173
183
  | Policy | Score |
@@ -185,6 +195,7 @@ facts and **tracks weak-but-true updates** on volatile ones. Full distributions:
185
195
  python examples/contradiction_demo.py
186
196
  python experiments/mem0_comparison.py
187
197
  python experiments/mem0_side_by_side.py # pip install mem0ai; OPENAI_API_KEY or MEM0_BACKEND=ollama
198
+ python experiments/voltmem_eval.py # 20/20 escalation probes + retrieval separation
188
199
  python experiments/memory_demo.py
189
200
  ```
190
201
 
@@ -225,9 +236,24 @@ bob = create_memory("app.db", user_id="bob")
225
236
  | `examples/contradiction_demo.py` | VoltMem vs always-add on contradictions |
226
237
  | `experiments/mem0_comparison.py` | 3-scenario head-to-head vs always-add |
227
238
  | `experiments/mem0_side_by_side.py` | 3-scenario head-to-head vs real Mem0 (3/3 wedge) |
239
+ | `experiments/voltmem_eval.py` | End-to-end escalation + retrieval (20/20 probes, real > flat > swap) |
240
+ | `experiments/calibrate_escalation.py` | Print E_t vs θ table for tuning explicit-override constants |
228
241
  | `examples/quickstart_batteries.py` | `remember()` / `recall()` low-level API |
229
242
  | `examples/multi_tenant.py` | One DB, many users |
230
243
  | `examples/langchain_agent.py` | LangChain adapter |
244
+ | `examples/chat_app/` | Memory-aware CLI chat (extendable to web UI) |
245
+ | `examples/custom_classifier.py` | Pluggable ``KeywordClassifier`` + ``DomainRegistry`` |
246
+
247
+ ### Chat app (CLI)
248
+
249
+ ```bash
250
+ pip install -e ".[embeddings]"
251
+ python -m examples.chat_app # REPL; uses Ollama if running, else echo mode
252
+ python -m examples.chat_app --demo # scripted smoke test
253
+ python -m examples.chat_app --show-recall
254
+ ```
255
+
256
+ Slash commands: `/memories`, `/search <query>`, `/clear`, `/reset`, `/help`.
231
257
 
232
258
  ---
233
259
 
@@ -238,11 +264,41 @@ bob = create_memory("app.db", user_id="bob")
238
264
  | `personality_trait` | 0.05 | Very protected |
239
265
  | `core_preference` | 0.08 | Very protected |
240
266
  | `biographical` | 0.10 | High protection |
267
+ | `professional_context` | 0.30 | Medium — job/role (career changes) |
268
+ | `location` | 0.60 | Updates readily (Berlin → Paris) |
241
269
  | `current_project` | 0.55 | Updates readily |
242
270
  | `emotional_context` | 0.80 | Fast-moving |
243
271
  | `current_task` | 0.90 | Minimal protection |
244
272
 
245
- Custom domains: `voltmem/domains.py`.
273
+ Custom domains: register via ``DomainRegistry`` and pass to ``create_memory(domains=...)``.
274
+ Pluggable classifiers: ``create_memory(classifier=...)`` — ``"heuristic"``, ``"llm"``, ``KeywordClassifier``, or a callable dict.
275
+
276
+ Optional: ``create_memory(..., auto_discover=True)`` learns per-domain volatility from
277
+ confirm / mismatch / supersede patterns (blended with priors above; cold-start applies).
278
+
279
+ ```python
280
+ from voltmem import create_memory, DomainRegistry, KeywordClassifier, ChainedClassifier, HeuristicClassifier
281
+
282
+ domains = DomainRegistry()
283
+ domains.register("style_preference", 0.08)
284
+ domains.register("style_constraint", 0.25)
285
+
286
+ mem = create_memory(
287
+ "app.db",
288
+ user_id="alice",
289
+ domains=domains,
290
+ classifier=ChainedClassifier([
291
+ KeywordClassifier({
292
+ "style_preference": ["prefer", "darker colors", "minimal"],
293
+ "style_constraint": ["no wool", "tight budget"],
294
+ }),
295
+ HeuristicClassifier(),
296
+ ]),
297
+ )
298
+
299
+ mem.add("I prefer darker colors and minimal fits")
300
+ hits = mem.search("what colors does the user like?")
301
+ ```
246
302
 
247
303
  ---
248
304
 
@@ -255,6 +311,7 @@ python tests/test_client.py
255
311
  ```
256
312
 
257
313
  Experiments and benchmarks live in `experiments/` — see [docs/RESEARCH.md](docs/RESEARCH.md).
314
+ Open problems and roadmap: [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md).
258
315
 
259
316
  ---
260
317
 
@@ -1,6 +1,6 @@
1
1
  # VoltMem
2
2
 
3
- [![PyPI](https://img.shields.io/pypi/v/voltmem)](https://pypi.org/project/voltmem/)
3
+ [![Version](https://img.shields.io/badge/version-0.2.1-blue)](https://pypi.org/project/voltmem/)
4
4
  [![Python](https://img.shields.io/pypi/pyversions/voltmem)](https://pypi.org/project/voltmem/)
5
5
  [![License: MIT](https://img.shields.io/pypi/l/voltmem)](https://github.com/Rouche01/voltmem/blob/main/LICENSE)
6
6
 
@@ -16,7 +16,7 @@ volatile memories rank lower at search time.
16
16
 
17
17
  > Mem0 remembers relevant facts. VoltMem remembers **current truth**.
18
18
 
19
- **Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md)
19
+ **Research & benchmarks:** [docs/RESEARCH.md](docs/RESEARCH.md) · **Known limits & roadmap:** [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md)
20
20
 
21
21
  ---
22
22
 
@@ -102,6 +102,7 @@ flowchart LR
102
102
  | User moves cities | Berlin and Paris both stored | **Updates** to current city |
103
103
  | Old project name in haystack | Ranks by similarity | **Down-ranks** stale volatile facts |
104
104
  | Confident wrong blip on stable pref | Often accepted | **Resists** corruption |
105
+ | Career / role change (medium-stable) | Often blocked or duplicated | **Updates** on strong explicit contradiction |
105
106
 
106
107
  ### Example results (reproducible)
107
108
 
@@ -137,6 +138,15 @@ Run locally with `pip install -e ".[embeddings]"`. Embeddings:
137
138
  **VoltMem clearer wins: 3/3.** Mem0 keeps contradictory facts; VoltMem updates volatile
138
139
  slots and protects stable prefs via domain volatility + slot-aware linking.
139
140
 
141
+ **`experiments/voltmem_eval.py`** — end-to-end escalation + retrieval (real vs flat vs swap):
142
+
143
+ | Battery | real profile | flat (equal V) | swap (inverted V) |
144
+ |---|---|---|---|
145
+ | A — selective updating | **20/20** | 15/20 | 7/20 |
146
+ | B — retrieval separation | **+0.589** | +0.202 | −0.267 |
147
+
148
+ Includes the `professional_context` career-change probe (strong explicit evidence → update).
149
+
140
150
  **`experiments/memory_demo.py`** — 3 final Q&A checks vs ground truth:
141
151
 
142
152
  | Policy | Score |
@@ -154,6 +164,7 @@ facts and **tracks weak-but-true updates** on volatile ones. Full distributions:
154
164
  python examples/contradiction_demo.py
155
165
  python experiments/mem0_comparison.py
156
166
  python experiments/mem0_side_by_side.py # pip install mem0ai; OPENAI_API_KEY or MEM0_BACKEND=ollama
167
+ python experiments/voltmem_eval.py # 20/20 escalation probes + retrieval separation
157
168
  python experiments/memory_demo.py
158
169
  ```
159
170
 
@@ -194,9 +205,24 @@ bob = create_memory("app.db", user_id="bob")
194
205
  | `examples/contradiction_demo.py` | VoltMem vs always-add on contradictions |
195
206
  | `experiments/mem0_comparison.py` | 3-scenario head-to-head vs always-add |
196
207
  | `experiments/mem0_side_by_side.py` | 3-scenario head-to-head vs real Mem0 (3/3 wedge) |
208
+ | `experiments/voltmem_eval.py` | End-to-end escalation + retrieval (20/20 probes, real > flat > swap) |
209
+ | `experiments/calibrate_escalation.py` | Print E_t vs θ table for tuning explicit-override constants |
197
210
  | `examples/quickstart_batteries.py` | `remember()` / `recall()` low-level API |
198
211
  | `examples/multi_tenant.py` | One DB, many users |
199
212
  | `examples/langchain_agent.py` | LangChain adapter |
213
+ | `examples/chat_app/` | Memory-aware CLI chat (extendable to web UI) |
214
+ | `examples/custom_classifier.py` | Pluggable ``KeywordClassifier`` + ``DomainRegistry`` |
215
+
216
+ ### Chat app (CLI)
217
+
218
+ ```bash
219
+ pip install -e ".[embeddings]"
220
+ python -m examples.chat_app # REPL; uses Ollama if running, else echo mode
221
+ python -m examples.chat_app --demo # scripted smoke test
222
+ python -m examples.chat_app --show-recall
223
+ ```
224
+
225
+ Slash commands: `/memories`, `/search <query>`, `/clear`, `/reset`, `/help`.
200
226
 
201
227
  ---
202
228
 
@@ -207,11 +233,41 @@ bob = create_memory("app.db", user_id="bob")
207
233
  | `personality_trait` | 0.05 | Very protected |
208
234
  | `core_preference` | 0.08 | Very protected |
209
235
  | `biographical` | 0.10 | High protection |
236
+ | `professional_context` | 0.30 | Medium — job/role (career changes) |
237
+ | `location` | 0.60 | Updates readily (Berlin → Paris) |
210
238
  | `current_project` | 0.55 | Updates readily |
211
239
  | `emotional_context` | 0.80 | Fast-moving |
212
240
  | `current_task` | 0.90 | Minimal protection |
213
241
 
214
- Custom domains: `voltmem/domains.py`.
242
+ Custom domains: register via ``DomainRegistry`` and pass to ``create_memory(domains=...)``.
243
+ Pluggable classifiers: ``create_memory(classifier=...)`` — ``"heuristic"``, ``"llm"``, ``KeywordClassifier``, or a callable dict.
244
+
245
+ Optional: ``create_memory(..., auto_discover=True)`` learns per-domain volatility from
246
+ confirm / mismatch / supersede patterns (blended with priors above; cold-start applies).
247
+
248
+ ```python
249
+ from voltmem import create_memory, DomainRegistry, KeywordClassifier, ChainedClassifier, HeuristicClassifier
250
+
251
+ domains = DomainRegistry()
252
+ domains.register("style_preference", 0.08)
253
+ domains.register("style_constraint", 0.25)
254
+
255
+ mem = create_memory(
256
+ "app.db",
257
+ user_id="alice",
258
+ domains=domains,
259
+ classifier=ChainedClassifier([
260
+ KeywordClassifier({
261
+ "style_preference": ["prefer", "darker colors", "minimal"],
262
+ "style_constraint": ["no wool", "tight budget"],
263
+ }),
264
+ HeuristicClassifier(),
265
+ ]),
266
+ )
267
+
268
+ mem.add("I prefer darker colors and minimal fits")
269
+ hits = mem.search("what colors does the user like?")
270
+ ```
215
271
 
216
272
  ---
217
273
 
@@ -224,6 +280,7 @@ python tests/test_client.py
224
280
  ```
225
281
 
226
282
  Experiments and benchmarks live in `experiments/` — see [docs/RESEARCH.md](docs/RESEARCH.md).
283
+ Open problems and roadmap: [docs/OPEN_PROBLEMS.md](docs/OPEN_PROBLEMS.md).
227
284
 
228
285
  ---
229
286
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voltmem"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "Current-truth memory for LLM agents — protect stable facts, update volatile ones."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,128 @@
1
+ """Tests for pluggable classifiers and DomainRegistry."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ ROOT = Path(__file__).resolve().parents[1]
8
+ sys.path.insert(0, str(ROOT))
9
+
10
+ from voltmem import ( # noqa: E402
11
+ ChainedClassifier,
12
+ DomainRegistry,
13
+ HeuristicClassifier,
14
+ KeywordClassifier,
15
+ create_memory,
16
+ )
17
+ from voltmem import domains as dom # noqa: E402
18
+
19
+
20
+ def test_keyword_classifier_matches_custom_domain():
21
+ domains = DomainRegistry()
22
+ domains.register("style_preference", 0.08)
23
+ restore = domains.install()
24
+ try:
25
+ clf = KeywordClassifier({
26
+ "style_preference": ["darker colors", "minimal style"],
27
+ })
28
+ assert clf.classify_domain("I prefer darker colors") == "style_preference"
29
+ assert clf.match_domain("hello there") is None
30
+ finally:
31
+ restore()
32
+
33
+
34
+ def test_chained_classifier_falls_through_to_heuristic():
35
+ domains = DomainRegistry()
36
+ domains.register("style_constraint", 0.25)
37
+ restore = domains.install()
38
+ try:
39
+ clf = ChainedClassifier([
40
+ KeywordClassifier({"style_constraint": ["no wool", "tight budget"]}),
41
+ HeuristicClassifier(),
42
+ ])
43
+ assert clf.classify_domain("no wool please") == "style_constraint"
44
+ assert clf.classify_domain("I live in Berlin") == "location"
45
+ finally:
46
+ restore()
47
+
48
+
49
+ def test_callable_classifier_dict():
50
+ with create_memory(
51
+ ":memory:",
52
+ user_id="u1",
53
+ embeddings=False,
54
+ classifier={
55
+ "classify": lambda t: "custom_domain"
56
+ if "stylens" in t.lower() else "stated_preference",
57
+ "mismatch": lambda n, e, s: 0.05 if s > 0.8 else 0.7,
58
+ },
59
+ domains=DomainRegistry().register("custom_domain", 0.4),
60
+ ) as mem:
61
+ row = mem.add("stylens user prefers navy")
62
+ assert row["domain"] == "custom_domain"
63
+
64
+
65
+ def test_create_memory_classifier_string_llm_alias():
66
+ # Should not raise — builds LLM classifier (may fall back on classify)
67
+ with create_memory(
68
+ ":memory:",
69
+ classifier="llm",
70
+ embeddings=False,
71
+ ) as mem:
72
+ assert mem.layer._extractor is not None
73
+
74
+
75
+ def test_domain_registry_restored_on_close():
76
+ before = dict(dom.DOMAIN_VOLATILITY)
77
+ reg = DomainRegistry().register("ephemeral_domain", 0.33, slot=True)
78
+ with create_memory(":memory:", domains=reg, embeddings=False) as mem:
79
+ assert "ephemeral_domain" in dom.DOMAIN_VOLATILITY
80
+ assert "ephemeral_domain" in dom.SLOT_DOMAINS
81
+ mem.add("ephemeral fact", extract=False)
82
+ assert dom.DOMAIN_VOLATILITY == before
83
+ assert "ephemeral_domain" not in dom.SLOT_DOMAINS
84
+
85
+
86
+ def test_domain_registry_custom_domain_classified():
87
+ domains = DomainRegistry()
88
+ domains.register("style_preference", 0.08)
89
+ classifier = KeywordClassifier({
90
+ "style_preference": ["darker colors", "minimal style"],
91
+ })
92
+ with create_memory(
93
+ ":memory:",
94
+ domains=domains,
95
+ classifier=classifier,
96
+ embeddings=False,
97
+ ) as mem:
98
+ row = mem.add("I love minimal style and darker colors")
99
+ assert row["domain"] == "style_preference"
100
+ assert "style_preference" in domains.known_domains()
101
+
102
+
103
+ def test_legacy_extractor_kwarg_still_works():
104
+ custom = HeuristicClassifier(relate_similarity=0.6)
105
+ with create_memory(":memory:", extractor=custom, embeddings=False) as mem:
106
+ assert mem.layer._extractor is custom
107
+
108
+
109
+ if __name__ == "__main__":
110
+ tests = [
111
+ test_keyword_classifier_matches_custom_domain,
112
+ test_chained_classifier_falls_through_to_heuristic,
113
+ test_callable_classifier_dict,
114
+ test_create_memory_classifier_string_llm_alias,
115
+ test_domain_registry_restored_on_close,
116
+ test_domain_registry_custom_domain_classified,
117
+ test_legacy_extractor_kwarg_still_works,
118
+ ]
119
+ failed = 0
120
+ for t in tests:
121
+ try:
122
+ t()
123
+ print(f" PASS {t.__name__}")
124
+ except Exception as exc:
125
+ failed += 1
126
+ print(f" FAIL {t.__name__}: {exc}")
127
+ print(f"\n{len(tests) - failed}/{len(tests)} passed")
128
+ raise SystemExit(1 if failed else 0)
@@ -0,0 +1,142 @@
1
+ """Tests for domain auto-discovery (Tier 1)."""
2
+
3
+ import os
4
+ import sys
5
+ import tempfile
6
+ from pathlib import Path
7
+
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ sys.path.insert(0, str(ROOT))
10
+
11
+ from voltmem import ( # noqa: E402
12
+ MemoryLayer,
13
+ DomainStats,
14
+ VolatilityTracker,
15
+ blend_volatility,
16
+ update_volatility_ema,
17
+ VOL_DRIFT_MAX,
18
+ DOMAIN_VOLATILITY,
19
+ )
20
+ from voltmem.domains import MemoryItem # noqa: E402
21
+ from voltmem.scoring import should_escalate # noqa: E402
22
+
23
+
24
+ def test_domain_stats_empirical_volatility_rises_with_supersedes():
25
+ stats = DomainStats(domain="location")
26
+ for _ in range(5):
27
+ stats.record("audited", mismatch=0.8)
28
+ assert stats.empirical_volatility > 0.4
29
+
30
+
31
+ def test_blend_volatility_requires_min_observations():
32
+ prior = 0.08
33
+ assert blend_volatility(prior, 0.9, n_observations=1) == prior
34
+ blended = blend_volatility(prior, 0.9, n_observations=10)
35
+ assert blended > prior
36
+
37
+
38
+ def test_prior_anchored_ema_clamps_drift():
39
+ item = MemoryItem(
40
+ id="x",
41
+ content="pref",
42
+ domain="personality_trait",
43
+ source="weak_inference",
44
+ volatility_ema=-1.0,
45
+ created_at=0.0,
46
+ last_confirmed_at=0.0,
47
+ )
48
+ prior = DOMAIN_VOLATILITY["personality_trait"]
49
+ updated = update_volatility_ema(item, observed_mismatch=0.9, source="weak_inference")
50
+ assert updated <= prior + VOL_DRIFT_MAX + 1e-9
51
+ assert updated >= prior - VOL_DRIFT_MAX - 1e-9
52
+
53
+
54
+ def test_auto_discover_records_observations():
55
+ with MemoryLayer(":memory:", auto_discover=True) as mem:
56
+ mem.write("I live in Berlin", domain="location")
57
+ mem.observe(
58
+ "I moved to Paris",
59
+ domain="location",
60
+ mismatch_magnitude=0.85,
61
+ source="explicit_statement",
62
+ )
63
+ summary = mem.summary()
64
+ assert summary["auto_discover"] is True
65
+ loc = summary["domain_discovery"]["location"]
66
+ assert loc["n_supersedes"] >= 1
67
+ assert loc["empirical"] > 0.0
68
+
69
+
70
+ def test_auto_discover_persists_across_sessions():
71
+ with tempfile.TemporaryDirectory() as tmp:
72
+ db = os.path.join(tmp, "discover.db")
73
+ with MemoryLayer(db, namespace="alice", auto_discover=True) as mem:
74
+ mem.write("I prefer concise answers", domain="core_preference")
75
+ for _ in range(4):
76
+ mem.observe(
77
+ "I prefer concise answers",
78
+ domain="core_preference",
79
+ mismatch_magnitude=0.05,
80
+ source="explicit_statement",
81
+ )
82
+ with MemoryLayer(db, namespace="alice", auto_discover=True) as mem2:
83
+ stats = mem2.summary()["domain_discovery"]["core_preference"]
84
+ assert stats["n_confirms"] >= 4
85
+
86
+
87
+ def test_auto_discover_off_by_default():
88
+ with MemoryLayer(":memory:") as mem:
89
+ assert mem.auto_discover is False
90
+ assert mem._tracker is None
91
+ mem.write("I live in Berlin", domain="location")
92
+ summary = mem.summary()
93
+ assert summary["domain_discovery"] == {}
94
+
95
+
96
+ def test_weak_inference_does_not_supersede_stable_with_auto_discover():
97
+ """Repeated weak contradictions should not corrupt a stable domain."""
98
+ with MemoryLayer(":memory:", auto_discover=True) as mem:
99
+ mem.write("User is a careful planner", domain="personality_trait")
100
+ for _ in range(8):
101
+ res = mem.observe(
102
+ "User did something out of character",
103
+ domain="personality_trait",
104
+ mismatch_magnitude=0.6,
105
+ source="weak_inference",
106
+ )
107
+ assert res.action == "logged_mismatch"
108
+ items = mem._active(domain="personality_trait")
109
+ assert len(items) == 1
110
+ assert "careful planner" in items[0].content
111
+
112
+
113
+ def test_create_memory_auto_discover_flag():
114
+ from voltmem import create_memory
115
+
116
+ with create_memory(":memory:", auto_discover=True, embeddings=False) as mem:
117
+ assert mem.layer.auto_discover is True
118
+ row = mem.add("I live in Berlin")
119
+ assert row["action"] == "inserted"
120
+
121
+
122
+ if __name__ == "__main__":
123
+ tests = [
124
+ test_domain_stats_empirical_volatility_rises_with_supersedes,
125
+ test_blend_volatility_requires_min_observations,
126
+ test_prior_anchored_ema_clamps_drift,
127
+ test_auto_discover_records_observations,
128
+ test_auto_discover_persists_across_sessions,
129
+ test_auto_discover_off_by_default,
130
+ test_weak_inference_does_not_supersede_stable_with_auto_discover,
131
+ test_create_memory_auto_discover_flag,
132
+ ]
133
+ failed = 0
134
+ for t in tests:
135
+ try:
136
+ t()
137
+ print(f" PASS {t.__name__}")
138
+ except Exception as exc:
139
+ failed += 1
140
+ print(f" FAIL {t.__name__}: {exc}")
141
+ print(f"\n{len(tests) - failed}/{len(tests)} passed")
142
+ raise SystemExit(1 if failed else 0)