mindforge-memory 6.4.5__tar.gz → 6.4.8__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.
- {mindforge_memory-6.4.5/mindforge_memory.egg-info → mindforge_memory-6.4.8}/PKG-INFO +23 -5
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/README.md +22 -4
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/indexer.py +123 -10
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/mindforge.py +14 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/query.py +26 -2
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/storage.py +33 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/version.py +2 -2
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8/mindforge_memory.egg-info}/PKG-INFO +23 -5
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/SOURCES.txt +3 -0
- mindforge_memory-6.4.8/tests/test_trash_hard_delete.py +115 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v568_fixes.py +1 -1
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v575_release_consistency.py +2 -2
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v612_fixes.py +2 -2
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v643_fixes.py +1 -1
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v645_fixes.py +2 -2
- mindforge_memory-6.4.8/tests/test_v646_fixes.py +92 -0
- mindforge_memory-6.4.8/tests/test_v648_fixes.py +189 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/LICENSE +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/MindForge.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/adapters/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/adapters/claude_adapter.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/adapters/crewai_memory.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/adapters/generic_api.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/adapters/langgraph_store.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/adapters/openclaw_adapter.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/api/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/api/server.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/cli/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/cli/main.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/embedding.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/encryption.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/paths.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/core/types.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/daemon/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/daemon/server.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/mcp/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/mcp/server.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/__init__.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/categorizer.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/conflict_detector.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/connectors.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/context_window.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/event_bus.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/evolution.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/experience.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/federated.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/federated_acl.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/follow_up.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/hybrid_search.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/importer.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/integrator.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/intent_router.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/knowledge_graph.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/memory_compressor.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/memory_decay.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/memory_passport.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/multi_agent.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/multimodal.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/outcome_learning.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/personality.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/principled_forgetting.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/privacy.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/recall.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/session_focus.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/share_conflict.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/skill_extractor.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/skill_pack.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/timeline_viz.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge/modules/v6.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/dependency_links.txt +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/entry_points.txt +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/requires.txt +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/top_level.txt +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/pyproject.toml +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/setup.cfg +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/setup.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_core.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_security_hardening.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v546_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v547_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v548_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v550_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v552_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v554_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v556_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v557_banner_insight_wal.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v557_security_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v557_webhook_sig_export.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v558_audit_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v558_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v560_decay_gc.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v560_kdf_versioning.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v560_module_coverage.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v561_security.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v562_p2_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v563_entry_coverage.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v563_perf_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v565_security.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v566_encrypted_search.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v567_security_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v569_fts_sync.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v570_coverage_expansion.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v570_drama_and_utils.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v570_encrypted_fallback.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v571_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v572_security.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v573_encrypted_export.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v573_security.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v576_bitemporal.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v576_gdpr.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v577_agent_governance.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v578_embedding.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v579_graph.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v580_experience.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v5812_bugfixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v583_security.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v584_adapters.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v585_benchmark.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v586_federated_mcp.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v590_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v592_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v593_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v595_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v600_forgetting_followup.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v600_import_timeline.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v600_multi_agent_formal.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v600_outcome_passport.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v600_skillpack_evolution.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v611_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v612_cascade_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v613_integrity.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v620_growth_governance.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v630_search_explain.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v631_scrypt.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v632_explain_interfaces.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v635_features.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v635_integration.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v636_fixes.py +0 -0
- {mindforge_memory-6.4.5 → mindforge_memory-6.4.8}/tests/test_v6_ports.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mindforge-memory
|
|
3
|
-
Version: 6.4.
|
|
3
|
+
Version: 6.4.8
|
|
4
4
|
Summary: AI Agent 终身记忆系统 - 四层记忆架构 · 知识图谱 · 多模态支持
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/opok-ops/MindForge
|
|
@@ -36,11 +36,11 @@ Dynamic: license-file
|
|
|
36
36
|
|
|
37
37
|
MindForge gives AI agents a persistent, structured long-term memory: a four-tier lifecycle model, knowledge graphs, hybrid retrieval, end-to-end encryption, and MCP / CLI / REST interfaces. All data stays on your machine.
|
|
38
38
|
|
|
39
|
-
> **The story** — one developer, seven months, from v1.0 (March 2026) to v6.4.
|
|
39
|
+
> **The story** — one developer, seven months, from v1.0 (March 2026) to v6.4.8: no team, no funding, 1,252 passing tests. [Read the journey →](https://opok-ops.github.io/MindForge/#developer)
|
|
40
40
|
|
|
41
41
|
[](https://www.python.org/downloads/)
|
|
42
42
|
[](LICENSE)
|
|
43
|
-
[](https://github.com/opok-ops/MindForge/releases)
|
|
44
44
|
[](https://github.com/opok-ops/MindForge/stargazers)
|
|
45
45
|
[](https://github.com/opok-ops/MindForge/forks)
|
|
46
46
|
[](https://github.com/opok-ops/MindForge/issues)
|
|
@@ -94,10 +94,28 @@ MindForge search "what does user like"
|
|
|
94
94
|
MindForge stats
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
|
|
98
|
+
## Scale & Quality Benchmarks (v6.4.8, measured)
|
|
99
|
+
|
|
100
|
+
| Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | search p50 (pathological all-common) | db size |
|
|
101
|
+
|---|---|---|---|---|---|
|
|
102
|
+
| 200 entries (baseline) | — | 1.8 ms | — | — | <1 MB |
|
|
103
|
+
| 10,000 | 1,080/s | ~9.5 ms | ~16 ms | — | 10 MB |
|
|
104
|
+
| 100,000 | ~582/s | 6.8 ms | 135 ms | 122 ms (−44%) | 98 MB |
|
|
105
|
+
|
|
106
|
+
- Pathological queries (all-common words, synthetic template data): engine-level steady-state **4.0ms at 30k** (was 3825ms, −99.9%) — inverted-index fallback is now shortest-chain-first with a hard traversal budget; the remaining 100k cost is FTS5 high-frequency-phrase scoring (v6.5 target, honestly documented).
|
|
107
|
+
- **Index-blindness fix (v6.4.8)**: TF-IDF vocabulary was locked to the first 5 documents since v6.4.6 — incremental adds indexed as empty vectors (TF-IDF effectively half-dead; previous numbers carried by FTS5/fuzzy). Fixed with incremental vocabulary growth; all pipeline numbers above are now *true full-pipeline* measurements.
|
|
108
|
+
- Retrieval quality (fixed-seed, `benchmarks/quality_bench.py`, 2k entries): needle Recall@1 = 1.0, MRR = 1.0, topic Recall@1 = 1.0, noise false-recall = 0.
|
|
109
|
+
- Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
|
|
110
|
+
- TF-IDF degraded mode process memory at 100k: ~700 MB. Index LRU cap: 100,000 docs (MINDFORGE_INDEX_MAX_DOCS).
|
|
111
|
+
|
|
97
112
|
## What's New
|
|
98
113
|
|
|
99
|
-
v6.1.3 – v6.4.
|
|
114
|
+
v6.1.3 – v6.4.8 shipped in ten days (2026-11-01 → 11-11; three releases in the last two days), guarded by **1,253 tests** (1,252 passing, 1 environment-skipped).
|
|
115
|
+
|
|
116
|
+
- **v6.4.8 — pathological query path & index-blindness fix: O(N) fallback eliminated, TF-IDF actually indexing** (2026-11-11). Two deeper findings beyond the v6.4.6 stress test: (1) the all-common-word fallback traversed *all* inverted chains at full length (O(N×dims), 209ms p50 at 100k), and the 0.5×N max_df threshold still let half-corpus chains into the active set; (2) **the TF-IDF vocabulary had been locked to the first 5 documents** — every incremental add after that indexed as an empty vector (the TF-IDF path was effectively half-dead since v6.4.6; published numbers were carried by FTS5/fuzzy). Fix, six cuts: max_df tightened 0.5×N→**0.05×N**; fallback semantics → **shortest-chain-first** (≤3 dims, per-chain cap 5,000, islice); fuzzy candidate budget (SQL cap 2,000 with exact-substring-first ordering); fuzzy gating (runs only on zero TF-IDF hits — the true typo-correction scenario; encrypted mode unaffected); `incremental_fit()` vocabulary growth for adds after fit (old vectors not recomputed — new tokens contribute zero on old docs mathematically); empty-query fallback-signal reset. 30k repro: pathological steady-state 3825→**4.0ms** (−99.9%), mixed pipeline 1607→1.2ms, fuzzy single-path −93%. 100k re-bench: pathological −44%, all numbers now true full-pipeline. Remaining bottleneck honestly documented: FTS5 high-frequency-phrase scoring (v6.5). New observable `_last_traversed` + deterministic budget contracts. 9 new guards.
|
|
100
117
|
|
|
118
|
+
- **v6.4.6 — scale validation & retrieval benchmarks** (2026-11-10). First 100,000-entry stress test: add throughput ~600/s (linear), ~98MB db, memory peak ~700MB (TF-IDF degraded mode), search p50 7.4ms (rare token, −92% vs pre-fix) / 143ms (realistic mixed); pathological all-common query 421→218ms (−48%). Fixed a real bottleneck found by the stress test: Chinese long-query p50 was 421ms — profile showed low-IDF common words degenerating the inverted index into full-corpus scans. Fix: two-tier pruning (relative-eps + standard IR max_df stop-wording for df > 0.5×N) + heapq.nlargest; pathological all-common-word queries (synthetic-template data only) honestly documented as known O(N) characteristic, realistic queries verified unaffected (8k A/B, 30k side-by-side: 43ms mixed vs 86ms pathological). Shipped reproducible benchmarks (`benchmarks/scale_bench.py`, `benchmarks/quality_bench.py`): needle Recall@1 = 1.0 / MRR = 1.0, zero noise false-recall. Full LLM-judged benchmarks (LoCoMo/LongMemEval) deferred — needs external LLM API key. 7 new guards.
|
|
101
119
|
- **v6.4.5 — audit-driven fixes** (2026-11-10; v6.4.4 skipped by maintainer decision). Two inverse doc-code inconsistencies (v6.4.3 fixed "claimed but not done"; this one fixes "done but not tracked"): `connectors.py` outgoing HTTP User-Agent was hardcoded `MindForge-Connector/5.8.9` — lying about the version to every target site since the 5.8.9 era (`event_bus.py` fixed the same disease in v5.6.8; this one slipped through). Now dynamic from the single version truth. Website "core modules" count was stale (17 in prose, 15 in stat card, 31 measured) — both fixed to the measured 31; MCP tool count 87 verified exact, untouched. Audit archive (not a defect): WAL auto-checkpoint verified healthy under daemon-like load — 4 rounds × 400 mixed ops, WAL converged at ~4.1MB (default 1000-page threshold), TRUNCATE resets to zero. 5 new guards in `tests/test_v645_fixes.py`.
|
|
102
120
|
- **v6.4.3 — documentation-code consistency fix** (2026-11-10). Pre-release audit found that v6.4.2's claimed first cut — WAL + `synchronous=NORMAL` — **never landed in code**: `_get_conn()` set only `journal_mode=WAL`, leaving `synchronous` at SQLite's default FULL. The 2.1× search gain was real, but carried entirely by the other four cuts — real numbers, misattributed cause. This release adds `PRAGMA synchronous=NORMAL` on the WAL branch (per-connection, cross-thread; network-FS DELETE fallback stays FULL by design). Same-machine A/B on SSD shows no statistically significant latency delta (search p50 1.8 → 1.8ms — fsync is ~0.1ms-scale here); the benefit targets slow disks and IO-saturated containers, and more importantly makes the documented power-loss semantics (un-checkpointed access counts / search audits may be lost; compliance audits still commit immediately) actually hold. 4 new guard tests in `tests/test_v643_fixes.py`.
|
|
103
121
|
- **v6.4.2 — tens-of-thousands-chars latency: another 2×** (2026-11-09). Same-machine A/B benchmark (400 memories / ~47k chars, 3 runs each): **search p50 6.3→3.0ms (2.1×), p95 11.7→4.2ms (2.8×)**; 24k-char library p50 2.5ms; fast machine state 1.8ms. Five surgical cuts: WAL + `synchronous=NORMAL` (commits skip per-write fsync), enum-downgrade memo, `row.keys()` set reuse, first-visit access-count batching merged into one `flush_access()` transaction per search, and search-audit INSERT folded into the same commit. The v5.6.3 throttle contract and v5.5.4 `recently_accessed` semantics are pinned by 23 contract tests — all green. Compliance-grade audits still commit immediately. *(Note: the `synchronous=NORMAL` cut landed in v6.4.3 — see above.)*
|
|
@@ -168,7 +186,7 @@ v6.1.3 – v6.4.5 shipped in ten days (2026-11-01 → 11-10; two same-day releas
|
|
|
168
186
|
|
|
169
187
|
```
|
|
170
188
|
┌──────────────────────────────────────────────────────┐
|
|
171
|
-
│ MindForge v6.4.
|
|
189
|
+
│ MindForge v6.4.8 │
|
|
172
190
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
173
191
|
│ MemoryEvolution · FederatedMemory │
|
|
174
192
|
├──────────────────────────────────────────────────────┤
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
MindForge gives AI agents a persistent, structured long-term memory: a four-tier lifecycle model, knowledge graphs, hybrid retrieval, end-to-end encryption, and MCP / CLI / REST interfaces. All data stays on your machine.
|
|
8
8
|
|
|
9
|
-
> **The story** — one developer, seven months, from v1.0 (March 2026) to v6.4.
|
|
9
|
+
> **The story** — one developer, seven months, from v1.0 (March 2026) to v6.4.8: no team, no funding, 1,252 passing tests. [Read the journey →](https://opok-ops.github.io/MindForge/#developer)
|
|
10
10
|
|
|
11
11
|
[](https://www.python.org/downloads/)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](https://github.com/opok-ops/MindForge/releases)
|
|
14
14
|
[](https://github.com/opok-ops/MindForge/stargazers)
|
|
15
15
|
[](https://github.com/opok-ops/MindForge/forks)
|
|
16
16
|
[](https://github.com/opok-ops/MindForge/issues)
|
|
@@ -64,10 +64,28 @@ MindForge search "what does user like"
|
|
|
64
64
|
MindForge stats
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
|
|
68
|
+
## Scale & Quality Benchmarks (v6.4.8, measured)
|
|
69
|
+
|
|
70
|
+
| Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | search p50 (pathological all-common) | db size |
|
|
71
|
+
|---|---|---|---|---|---|
|
|
72
|
+
| 200 entries (baseline) | — | 1.8 ms | — | — | <1 MB |
|
|
73
|
+
| 10,000 | 1,080/s | ~9.5 ms | ~16 ms | — | 10 MB |
|
|
74
|
+
| 100,000 | ~582/s | 6.8 ms | 135 ms | 122 ms (−44%) | 98 MB |
|
|
75
|
+
|
|
76
|
+
- Pathological queries (all-common words, synthetic template data): engine-level steady-state **4.0ms at 30k** (was 3825ms, −99.9%) — inverted-index fallback is now shortest-chain-first with a hard traversal budget; the remaining 100k cost is FTS5 high-frequency-phrase scoring (v6.5 target, honestly documented).
|
|
77
|
+
- **Index-blindness fix (v6.4.8)**: TF-IDF vocabulary was locked to the first 5 documents since v6.4.6 — incremental adds indexed as empty vectors (TF-IDF effectively half-dead; previous numbers carried by FTS5/fuzzy). Fixed with incremental vocabulary growth; all pipeline numbers above are now *true full-pipeline* measurements.
|
|
78
|
+
- Retrieval quality (fixed-seed, `benchmarks/quality_bench.py`, 2k entries): needle Recall@1 = 1.0, MRR = 1.0, topic Recall@1 = 1.0, noise false-recall = 0.
|
|
79
|
+
- Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
|
|
80
|
+
- TF-IDF degraded mode process memory at 100k: ~700 MB. Index LRU cap: 100,000 docs (MINDFORGE_INDEX_MAX_DOCS).
|
|
81
|
+
|
|
67
82
|
## What's New
|
|
68
83
|
|
|
69
|
-
v6.1.3 – v6.4.
|
|
84
|
+
v6.1.3 – v6.4.8 shipped in ten days (2026-11-01 → 11-11; three releases in the last two days), guarded by **1,253 tests** (1,252 passing, 1 environment-skipped).
|
|
85
|
+
|
|
86
|
+
- **v6.4.8 — pathological query path & index-blindness fix: O(N) fallback eliminated, TF-IDF actually indexing** (2026-11-11). Two deeper findings beyond the v6.4.6 stress test: (1) the all-common-word fallback traversed *all* inverted chains at full length (O(N×dims), 209ms p50 at 100k), and the 0.5×N max_df threshold still let half-corpus chains into the active set; (2) **the TF-IDF vocabulary had been locked to the first 5 documents** — every incremental add after that indexed as an empty vector (the TF-IDF path was effectively half-dead since v6.4.6; published numbers were carried by FTS5/fuzzy). Fix, six cuts: max_df tightened 0.5×N→**0.05×N**; fallback semantics → **shortest-chain-first** (≤3 dims, per-chain cap 5,000, islice); fuzzy candidate budget (SQL cap 2,000 with exact-substring-first ordering); fuzzy gating (runs only on zero TF-IDF hits — the true typo-correction scenario; encrypted mode unaffected); `incremental_fit()` vocabulary growth for adds after fit (old vectors not recomputed — new tokens contribute zero on old docs mathematically); empty-query fallback-signal reset. 30k repro: pathological steady-state 3825→**4.0ms** (−99.9%), mixed pipeline 1607→1.2ms, fuzzy single-path −93%. 100k re-bench: pathological −44%, all numbers now true full-pipeline. Remaining bottleneck honestly documented: FTS5 high-frequency-phrase scoring (v6.5). New observable `_last_traversed` + deterministic budget contracts. 9 new guards.
|
|
70
87
|
|
|
88
|
+
- **v6.4.6 — scale validation & retrieval benchmarks** (2026-11-10). First 100,000-entry stress test: add throughput ~600/s (linear), ~98MB db, memory peak ~700MB (TF-IDF degraded mode), search p50 7.4ms (rare token, −92% vs pre-fix) / 143ms (realistic mixed); pathological all-common query 421→218ms (−48%). Fixed a real bottleneck found by the stress test: Chinese long-query p50 was 421ms — profile showed low-IDF common words degenerating the inverted index into full-corpus scans. Fix: two-tier pruning (relative-eps + standard IR max_df stop-wording for df > 0.5×N) + heapq.nlargest; pathological all-common-word queries (synthetic-template data only) honestly documented as known O(N) characteristic, realistic queries verified unaffected (8k A/B, 30k side-by-side: 43ms mixed vs 86ms pathological). Shipped reproducible benchmarks (`benchmarks/scale_bench.py`, `benchmarks/quality_bench.py`): needle Recall@1 = 1.0 / MRR = 1.0, zero noise false-recall. Full LLM-judged benchmarks (LoCoMo/LongMemEval) deferred — needs external LLM API key. 7 new guards.
|
|
71
89
|
- **v6.4.5 — audit-driven fixes** (2026-11-10; v6.4.4 skipped by maintainer decision). Two inverse doc-code inconsistencies (v6.4.3 fixed "claimed but not done"; this one fixes "done but not tracked"): `connectors.py` outgoing HTTP User-Agent was hardcoded `MindForge-Connector/5.8.9` — lying about the version to every target site since the 5.8.9 era (`event_bus.py` fixed the same disease in v5.6.8; this one slipped through). Now dynamic from the single version truth. Website "core modules" count was stale (17 in prose, 15 in stat card, 31 measured) — both fixed to the measured 31; MCP tool count 87 verified exact, untouched. Audit archive (not a defect): WAL auto-checkpoint verified healthy under daemon-like load — 4 rounds × 400 mixed ops, WAL converged at ~4.1MB (default 1000-page threshold), TRUNCATE resets to zero. 5 new guards in `tests/test_v645_fixes.py`.
|
|
72
90
|
- **v6.4.3 — documentation-code consistency fix** (2026-11-10). Pre-release audit found that v6.4.2's claimed first cut — WAL + `synchronous=NORMAL` — **never landed in code**: `_get_conn()` set only `journal_mode=WAL`, leaving `synchronous` at SQLite's default FULL. The 2.1× search gain was real, but carried entirely by the other four cuts — real numbers, misattributed cause. This release adds `PRAGMA synchronous=NORMAL` on the WAL branch (per-connection, cross-thread; network-FS DELETE fallback stays FULL by design). Same-machine A/B on SSD shows no statistically significant latency delta (search p50 1.8 → 1.8ms — fsync is ~0.1ms-scale here); the benefit targets slow disks and IO-saturated containers, and more importantly makes the documented power-loss semantics (un-checkpointed access counts / search audits may be lost; compliance audits still commit immediately) actually hold. 4 new guard tests in `tests/test_v643_fixes.py`.
|
|
73
91
|
- **v6.4.2 — tens-of-thousands-chars latency: another 2×** (2026-11-09). Same-machine A/B benchmark (400 memories / ~47k chars, 3 runs each): **search p50 6.3→3.0ms (2.1×), p95 11.7→4.2ms (2.8×)**; 24k-char library p50 2.5ms; fast machine state 1.8ms. Five surgical cuts: WAL + `synchronous=NORMAL` (commits skip per-write fsync), enum-downgrade memo, `row.keys()` set reuse, first-visit access-count batching merged into one `flush_access()` transaction per search, and search-audit INSERT folded into the same commit. The v5.6.3 throttle contract and v5.5.4 `recently_accessed` semantics are pinned by 23 contract tests — all green. Compliance-grade audits still commit immediately. *(Note: the `synchronous=NORMAL` cut landed in v6.4.3 — see above.)*
|
|
@@ -138,7 +156,7 @@ v6.1.3 – v6.4.5 shipped in ten days (2026-11-01 → 11-10; two same-day releas
|
|
|
138
156
|
|
|
139
157
|
```
|
|
140
158
|
┌──────────────────────────────────────────────────────┐
|
|
141
|
-
│ MindForge v6.4.
|
|
159
|
+
│ MindForge v6.4.8 │
|
|
142
160
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
143
161
|
│ MemoryEvolution · FederatedMemory │
|
|
144
162
|
├──────────────────────────────────────────────────────┤
|
|
@@ -3,11 +3,13 @@ MindForge v5.0 索引引擎
|
|
|
3
3
|
支持 TF-IDF、向量索引、FTS5 全文检索
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import heapq
|
|
6
7
|
import math
|
|
7
8
|
import os
|
|
8
9
|
import re
|
|
9
10
|
import sqlite3
|
|
10
11
|
from collections import Counter, defaultdict
|
|
12
|
+
from itertools import islice
|
|
11
13
|
from typing import List, Dict, Tuple, Optional
|
|
12
14
|
from dataclasses import dataclass
|
|
13
15
|
|
|
@@ -27,6 +29,7 @@ class TFIDFVectorizer:
|
|
|
27
29
|
def __init__(self):
|
|
28
30
|
self.vocab: Dict[str, int] = {}
|
|
29
31
|
self.idf: Dict[str, float] = {}
|
|
32
|
+
self._df: Dict[str, int] = {} # v6.4.8: 增量 fit 的精确 df 延续
|
|
30
33
|
self.doc_count: int = 0
|
|
31
34
|
|
|
32
35
|
def _tokenize(self, text: str) -> List[str]:
|
|
@@ -58,6 +61,33 @@ class TFIDFVectorizer:
|
|
|
58
61
|
|
|
59
62
|
for word, idx in self.vocab.items():
|
|
60
63
|
self.idf[word] = math.log((self.doc_count + 1) / (df[word] + 1)) + 1
|
|
64
|
+
# v6.4.8: df 表留存,供 incremental_fit 延续精确计数
|
|
65
|
+
self._df = dict(df)
|
|
66
|
+
|
|
67
|
+
def incremental_fit(self, text: str) -> None:
|
|
68
|
+
"""v6.4.8 新增:单文档增量词表扩展。
|
|
69
|
+
|
|
70
|
+
修复「fit 后新增文档失索引」:新 token 按当前 doc_count+1 估
|
|
71
|
+
IDF 补进词表;已知 token 的 df 计数 +1(IDF 漂移不回算旧向量,
|
|
72
|
+
见 index_memory 注释)。维度号从当前词表长度起追加,与既有
|
|
73
|
+
向量维度兼容(旧文档在新词上恒为零分量)。
|
|
74
|
+
"""
|
|
75
|
+
if not self._df:
|
|
76
|
+
# 未 fit 过(不应发生——调用方保证 fitted),防御性直通
|
|
77
|
+
return
|
|
78
|
+
self.doc_count += 1
|
|
79
|
+
next_idx = len(self.vocab)
|
|
80
|
+
for token in set(self._tokenize(text)):
|
|
81
|
+
if token in self.vocab:
|
|
82
|
+
self._df[token] = self._df.get(token, 0) + 1
|
|
83
|
+
self.idf[token] = math.log(
|
|
84
|
+
(self.doc_count + 1) / (self._df[token] + 1)) + 1
|
|
85
|
+
else:
|
|
86
|
+
self._df[token] = 1
|
|
87
|
+
self.vocab[token] = next_idx
|
|
88
|
+
next_idx += 1
|
|
89
|
+
self.idf[token] = math.log(
|
|
90
|
+
(self.doc_count + 1) / 2) + 1
|
|
61
91
|
|
|
62
92
|
def transform(self, text: str) -> Dict[int, float]:
|
|
63
93
|
tokens = self._tokenize(text)
|
|
@@ -107,6 +137,10 @@ class VectorIndex:
|
|
|
107
137
|
self.metadata: Dict[str, Dict] = {}
|
|
108
138
|
self._norms: Dict[str, float] = {} # 预计算 L2 范数缓存
|
|
109
139
|
self._postings: Dict[int, Dict[str, float]] = {} # 倒排链
|
|
140
|
+
# v6.4.8: 上次 search 实际遍历的倒排链条目数(性能契约可观测)
|
|
141
|
+
self._last_traversed = 0
|
|
142
|
+
# v6.4.8: 上次 search 是否走了病态回退(全库词查询信号)
|
|
143
|
+
self.last_fallback = False
|
|
110
144
|
|
|
111
145
|
@staticmethod
|
|
112
146
|
def _as_sparse(vector) -> Dict[int, float]:
|
|
@@ -149,19 +183,81 @@ class VectorIndex:
|
|
|
149
183
|
q = self._as_sparse(query_vector)
|
|
150
184
|
query_norm = math.sqrt(sum(v * v for v in q.values()))
|
|
151
185
|
if query_norm == 0:
|
|
186
|
+
# v6.4.8: 空查询(无 token/全 vocab miss)≠ 全库词回退。
|
|
187
|
+
# 显式复位 last_fallback,防止继承上一查询的 True 导致
|
|
188
|
+
# query.py 误跳过 fuzzy 拼写兜底(空查询正是 fuzzy 的
|
|
189
|
+
# 设计场景:用户词打错 → 全 miss → fuzzy 纠错召回)。
|
|
190
|
+
self.last_fallback = False
|
|
191
|
+
self._last_traversed = 0
|
|
152
192
|
return [(doc_id, 0.0) for doc_id in list(self.vectors.keys())[:top_k]]
|
|
153
193
|
|
|
194
|
+
# v6.4.6 perf: 低区分度查询维剪枝。100k 规模压测发现中文长查询
|
|
195
|
+
# p50 421ms,profile 定位到常见词(如「条目/陈述性/记忆」)的倒排
|
|
196
|
+
# 链全量遍历。两层剪枝:eps 相对阈值 + max_df 停用词。
|
|
197
|
+
# v6.4.8 perf: max_df 从 0.5×N 收紧到 0.05×N。0.5 时半库词链
|
|
198
|
+
# (数十万级遍历)仍进 active,100k 实测 mixed_realistic p50
|
|
199
|
+
# 134ms。常见词的点积贡献近似常数(30k profile 实证 eps 剪不掉
|
|
200
|
+
# 全库词,v6.4.6 注释已论证 top-K 排序不变),链长超库容 5% 的
|
|
201
|
+
# 维一律剪——100k 库下 active 链长上界 5k,遍历毫秒级。
|
|
202
|
+
_EPS_REL = 0.02
|
|
203
|
+
_max_df_len = 0.05 * len(self.vectors)
|
|
204
|
+
active = [(idx, qval) for idx, qval in q.items()
|
|
205
|
+
if qval > _EPS_REL * query_norm
|
|
206
|
+
and len(self._postings.get(idx, ())) <= _max_df_len]
|
|
207
|
+
self._last_traversed = 0
|
|
208
|
+
# v6.4.8: 病态回退信号——查询全部维度超 max_df(全库词)时
|
|
209
|
+
# 置位,供上层 QueryEngine 跳过 fuzzy 兜底路(全库词查询对
|
|
210
|
+
# fuzzy 的边际价值为零,详见 query.py 路 2 注释)。
|
|
211
|
+
self.last_fallback = False
|
|
212
|
+
if not active:
|
|
213
|
+
self.last_fallback = True
|
|
214
|
+
# v6.4.8 perf: 病态回退(全维被剪)不再遍历全部倒排链。
|
|
215
|
+
# 旧回退 list(q.items()) = O(N×查询维),100k 全库词查询
|
|
216
|
+
# p50 209ms 的根因。全库词贡献是常数,top-K 只由相对区分
|
|
217
|
+
# 度决定,因此回退语义改为「最短链优先」:
|
|
218
|
+
# 1) 按链长升序取最短 _FALLBACK_DIMS 维(q 中区分度最高
|
|
219
|
+
# 的信号,哪怕也被 max_df 剪掉)
|
|
220
|
+
# 2) 单链遍历截断到 _TRAVERSE_CAP 项——全平分数下截断
|
|
221
|
+
# 不改 top-K 语义,优先守住延迟上界
|
|
222
|
+
_FALLBACK_DIMS = 3
|
|
223
|
+
_TRAVERSE_CAP = 5000
|
|
224
|
+
ranked = sorted(
|
|
225
|
+
q.items(),
|
|
226
|
+
key=lambda kv: len(self._postings.get(kv[0], ())))
|
|
227
|
+
active = ranked[:_FALLBACK_DIMS]
|
|
228
|
+
# islice 免建截断副本(dict(list(x)[:cap]) 的 15k 次 hash
|
|
229
|
+
# 插入在实测里占 ~3ms,30k 病态复现 14→11ms)
|
|
230
|
+
dots: Dict[str, float] = {}
|
|
231
|
+
for idx, qval in active:
|
|
232
|
+
chain = self._postings.get(idx, {})
|
|
233
|
+
pairs = (chain.items() if len(chain) <= _TRAVERSE_CAP
|
|
234
|
+
else islice(chain.items(), _TRAVERSE_CAP))
|
|
235
|
+
for doc_id, dval in pairs:
|
|
236
|
+
dots[doc_id] = dots.get(doc_id, 0.0) + qval * dval
|
|
237
|
+
self._last_traversed += 1
|
|
238
|
+
return self._topk_from_dots(dots, query_norm, top_k)
|
|
239
|
+
|
|
154
240
|
# 倒排链累加点积:只访问与查询共享非零维度的候选文档
|
|
241
|
+
# (active 链长上界 _max_df_len,遍历量与库容解耦)
|
|
155
242
|
dots: Dict[str, float] = {}
|
|
156
|
-
for idx, qval in
|
|
243
|
+
for idx, qval in active:
|
|
157
244
|
for doc_id, dval in self._postings.get(idx, {}).items():
|
|
158
245
|
dots[doc_id] = dots.get(doc_id, 0.0) + qval * dval
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
246
|
+
self._last_traversed += 1
|
|
247
|
+
|
|
248
|
+
# v6.4.6 perf: heapq.nlargest 替代全量 sort——候选集大时
|
|
249
|
+
# O(n·log k) 优于 O(n·log n),且免建全量列表排序的中间开销。
|
|
250
|
+
return self._topk_from_dots(dots, query_norm, top_k)
|
|
251
|
+
|
|
252
|
+
def _topk_from_dots(self, dots: Dict[str, float], query_norm: float,
|
|
253
|
+
top_k: int) -> List[Tuple[str, float]]:
|
|
254
|
+
"""点积累积结果归一化 + top-K 选取(v6.4.8 抽出,回退/正常路径共用)"""
|
|
255
|
+
return heapq.nlargest(
|
|
256
|
+
top_k,
|
|
257
|
+
((doc_id, dot / (query_norm * self._norms[doc_id]))
|
|
258
|
+
for doc_id, dot in dots.items()
|
|
259
|
+
if self._norms.get(doc_id, 0.0) > 0),
|
|
260
|
+
key=lambda x: x[1])
|
|
165
261
|
|
|
166
262
|
def _cosine(self, v1, v2) -> float:
|
|
167
263
|
"""保留原始接口(向后兼容),稀疏/稠密输入均支持"""
|
|
@@ -193,6 +289,8 @@ class IndexEngine:
|
|
|
193
289
|
self.db_path = db_path
|
|
194
290
|
self.vectorizer = TFIDFVectorizer()
|
|
195
291
|
self.vector_index = VectorIndex()
|
|
292
|
+
# v6.4.8: 病态回退信号门面透传(见 search())
|
|
293
|
+
self.last_fallback = False
|
|
196
294
|
self._fitted = False
|
|
197
295
|
self._hydrated = False
|
|
198
296
|
self._doc_texts: Dict[str, str] = {}
|
|
@@ -250,12 +348,21 @@ class IndexEngine:
|
|
|
250
348
|
"""索引记忆"""
|
|
251
349
|
self._doc_texts[doc_id] = text
|
|
252
350
|
|
|
351
|
+
# v5.6.5 P1 #5 的渐进触发点保留(LRU 守卫契约:add 即索引,
|
|
352
|
+
# _doc_texts/vectors/postings 三结构同步有界)。
|
|
353
|
+
# v6.4.8 fix(祖传深坑):旧逻辑这里 fit 一次后,后续增量 add 的
|
|
354
|
+
# transform 全部 vocab miss → 空向量入索引(文档「失索引」)——
|
|
355
|
+
# 词表锁死在前 5 条文档的 token,100k 压测的 TF-IDF 路实际长期
|
|
356
|
+
# 半瘫痪(延迟数字由 FTS5/fuzzy 扛)。修复:fit 后的增量 add 先
|
|
357
|
+
# 走 incremental_fit 扩词表(新 token 补 vocab + df/idf 增量,
|
|
358
|
+
# 旧文档向量不回算——新词在旧文档 tf=0 无贡献,旧词 IDF 漂移为
|
|
359
|
+
# 可接受近似),再 transform 入索引。
|
|
253
360
|
if not self._fitted and len(self._doc_texts) >= 5:
|
|
254
361
|
self._fit_vectorizer()
|
|
362
|
+
elif self._fitted:
|
|
363
|
+
self.vectorizer.incremental_fit(text)
|
|
255
364
|
|
|
256
365
|
if self._fitted:
|
|
257
|
-
# v5.6.3 性能修复:直接存稀疏向量({dim: weight}),不再展开为
|
|
258
|
-
# 词表长度的稠密 list
|
|
259
366
|
vector_dict = self.vectorizer.transform(text)
|
|
260
367
|
self.vector_index.add(doc_id, vector_dict, metadata)
|
|
261
368
|
|
|
@@ -273,11 +380,17 @@ class IndexEngine:
|
|
|
273
380
|
self._fit_vectorizer()
|
|
274
381
|
|
|
275
382
|
if not self._fitted:
|
|
383
|
+
# v6.4.8: 降级路径同样透传病态回退信号(保持门面契约一致)
|
|
384
|
+
self.last_fallback = False
|
|
276
385
|
return self._keyword_search(query, top_k)
|
|
277
386
|
|
|
278
387
|
# v5.6.3 性能修复:查询向量同样保持稀疏,走倒排链检索
|
|
279
388
|
query_vec_dict = self.vectorizer.transform(query)
|
|
280
|
-
|
|
389
|
+
results = self.vector_index.search(query_vec_dict, top_k)
|
|
390
|
+
# v6.4.8: 把 VectorIndex 的病态回退信号透传到门面层
|
|
391
|
+
# (query.py 据此跳过 fuzzy 兜底路)
|
|
392
|
+
self.last_fallback = self.vector_index.last_fallback
|
|
393
|
+
return results
|
|
281
394
|
|
|
282
395
|
def _keyword_search(self, query: str, top_k: int) -> List[Tuple[str, float]]:
|
|
283
396
|
"""关键词搜索(降级方案)"""
|
|
@@ -1230,6 +1230,20 @@ class MindForge:
|
|
|
1230
1230
|
# v5.6.2 安全修复:隐私访问控制
|
|
1231
1231
|
entry = self._storage.get_memory(memory_id, actor, session_id)
|
|
1232
1232
|
if entry is None:
|
|
1233
|
+
# v6.4.6 fix: 硬删已软删(trash)条目的数据主权路径。get_memory
|
|
1234
|
+
# 过滤 trash 条目,此前 delete(hard_delete=True) 对回收站内容
|
|
1235
|
+
# 静默返回 False(CLI 显示「记忆不存在」,误导用户以为已彻底
|
|
1236
|
+
# 清除,物理数据实际仍在库中)。GDPR erase 语义(storage 权利
|
|
1237
|
+
# 清单 2122 行)要求:能软删活条目,就必须能物理清除其 trash
|
|
1238
|
+
# 态。隐私读检查不适用此分支——内容已在 trash,仅物理清除;
|
|
1239
|
+
# storage.delete_memory 自带审计写入(v5.6.1 同事务)。
|
|
1240
|
+
if hard_delete:
|
|
1241
|
+
if self._storage.is_in_trash(memory_id):
|
|
1242
|
+
success = self._storage.delete_memory(
|
|
1243
|
+
memory_id, actor, session_id, hard_delete=True)
|
|
1244
|
+
if success:
|
|
1245
|
+
self._index.remove_memory(memory_id)
|
|
1246
|
+
return success
|
|
1233
1247
|
return False
|
|
1234
1248
|
ok, _reason = self.privacy_engine.check_access(entry, actor, session_id)
|
|
1235
1249
|
if not ok:
|
|
@@ -259,9 +259,29 @@ class QueryEngine:
|
|
|
259
259
|
|
|
260
260
|
# 路 2:Fuzzy(TF-IDF 召回不足时补充;v5.7.1:加密模式下总是跑,
|
|
261
261
|
# 供加密兜底复用结果,避免二次全表扫描)
|
|
262
|
-
|
|
262
|
+
# v6.4.8 perf/语义: 触发条件收紧为「TF-IDF 零命中」:
|
|
263
|
+
# 1) 病态回退(index.last_fallback,全库词查询)→ 跳过。fuzzy
|
|
264
|
+
# 的设计价值是「拼写纠错/近似词」兜底,全库词查询里不存在
|
|
265
|
+
# 「没被 TF-IDF 命中的稀有词」,fuzzy 只会给出与全库同分数
|
|
266
|
+
# 的噪声结果(边际价值为零);
|
|
267
|
+
# 2) TF-IDF 已有命中(含低分——cosine 归一化下模板语料的命中
|
|
268
|
+
# 常在 0.2-0.3 边缘,但词命中本身已证明「查询词在库」)→
|
|
269
|
+
# 跳过。fuzzy 兜底的真场景是「查询词全不在库」(打错字),
|
|
270
|
+
# 此时 TF-IDF 必然空结果。mixed 实测(「稀有锚+常见词」):
|
|
271
|
+
# TF-IDF 15 命中 + FTS5 30 命中已足够填 top-K,fuzzy 跑
|
|
272
|
+
# cap-2000 候选纯属白税(30k 实测 140ms/查询)。
|
|
273
|
+
# 3) 性能——fuzzy 候选集对无区分度/长查询会放大成数万条
|
|
274
|
+
# (短条目库 LENGTH<4×len(q) 全体进候选),见 storage.py
|
|
275
|
+
# v6.4.8 候选预算注释。encrypted 模式不受此影响(仍全跑
|
|
276
|
+
# 供加密兜底复用)。
|
|
277
|
+
tfidf_fallback = getattr(self.index, "last_fallback", False)
|
|
278
|
+
# v6.4.8: 「有效命中」= TF-IDF 存在非零分结果。空查询(全 miss/
|
|
279
|
+
# 零范数)走文档序 0 分 top-K 兼容分支,tfidf_results 恒非空但
|
|
280
|
+
# 全零分——必须按分数判定,否则 fuzzy 拼写兜底被死锁。
|
|
281
|
+
tfidf_effective = any(s > 0.0 for _, s in tfidf_results)
|
|
263
282
|
fuzzy_hits = []
|
|
264
|
-
if self.storage.encrypted
|
|
283
|
+
if (self.storage.encrypted
|
|
284
|
+
or (not tfidf_effective and not tfidf_fallback)):
|
|
265
285
|
fuzzy_hits = self.storage.fuzzy_search(
|
|
266
286
|
query, limit=max_results * 2, threshold=0.1)
|
|
267
287
|
for item in fuzzy_hits:
|
|
@@ -276,6 +296,10 @@ class QueryEngine:
|
|
|
276
296
|
_bd(entry.id)["final"] = s
|
|
277
297
|
|
|
278
298
|
# 路 3:FTS5 全文检索
|
|
299
|
+
# v6.4.8 perf note: 尝试过「TF-IDF 有命中时跳过 FTS5」的门控,
|
|
300
|
+
# 被混合质量基线测试拦下(hybrid 0.875 < single 0.915,FTS5 的
|
|
301
|
+
# 补充分对融合质量有真实贡献)。正确方向是 FTS5 内部的
|
|
302
|
+
# ORDER BY rank top-k 优化(见 indexer.fts_search)。
|
|
279
303
|
fts5_used = False
|
|
280
304
|
# v5.6.2 安全修复:初始化默认值,防止异常路径下 UnboundLocalError
|
|
281
305
|
strategy_used = "tfidf+fuzzy"
|
|
@@ -2959,6 +2959,20 @@ class StorageEngine:
|
|
|
2959
2959
|
return len(ids)
|
|
2960
2960
|
|
|
2961
2961
|
@_with_rollback
|
|
2962
|
+
def is_in_trash(self, entry_id: str) -> bool:
|
|
2963
|
+
"""v6.4.6: 判定条目是否处于回收站(软删态)。
|
|
2964
|
+
|
|
2965
|
+
服务 delete(hard_delete=True) 的数据主权路径:get_memory 过滤
|
|
2966
|
+
trash 条目(WHERE category != 'trash'),上层 None 分支需要此
|
|
2967
|
+
轻量查询放行物理清除。幂等,只读,不产生任何写副作用。
|
|
2968
|
+
"""
|
|
2969
|
+
conn = self._get_conn()
|
|
2970
|
+
row = conn.execute(
|
|
2971
|
+
"SELECT 1 FROM memories WHERE id = ? AND category = 'trash'",
|
|
2972
|
+
(entry_id,),
|
|
2973
|
+
).fetchone()
|
|
2974
|
+
return row is not None
|
|
2975
|
+
|
|
2962
2976
|
def delete_memory(self, entry_id: str,
|
|
2963
2977
|
actor: str = "", session_id: str = "",
|
|
2964
2978
|
hard_delete: bool = False) -> bool:
|
|
@@ -9113,6 +9127,25 @@ class StorageEngine:
|
|
|
9113
9127
|
params_or.append(f"%{_ch}%")
|
|
9114
9128
|
cand_query = base_query + " AND (" + " OR ".join(conds) + ")"
|
|
9115
9129
|
|
|
9130
|
+
# v6.4.8 perf: 候选集预算上限 + 区分度优先排序。
|
|
9131
|
+
# 病理:短条目库(content < 4×len(query) 全体进候选)+ 16 字级
|
|
9132
|
+
# 中文长查询 → 候选≈全库,30k 实测 3 万条逐条反序列化+SequenceMatcher
|
|
9133
|
+
# = 1642ms(100k 语料条目 50-70 字时约半库过长度阈值,候选 ~5 万
|
|
9134
|
+
# = 212ms 白税)。预算语义(与 TF-IDF max_df 0.05×N 同一哲学):
|
|
9135
|
+
# - 正常查询(含区分词)LIKE 命中 + 短文本全体仍远小于预算,
|
|
9136
|
+
# 结果零变化;
|
|
9137
|
+
# - 候选超预算只发生在「查询对全库无区分度」的病态场景——此时
|
|
9138
|
+
# fuzzy 全体候选分数趋同,截断不改 top-K 语义(保延迟优先)。
|
|
9139
|
+
# 排序:content LIKE %q%(精确子串命中,最强 fuzzy 信号——打错字
|
|
9140
|
+
# 场景「fact-7770」vs「fact-777」也走这条路先保住)排最前,其余
|
|
9141
|
+
# 按 rowid(插入序)。截断发生在「短文本兜底路」而不是真命中。
|
|
9142
|
+
# encrypted 兜底路径同享此预算(候选爆炸对加密库同样不可接受)。
|
|
9143
|
+
_FUZZY_CAND_CAP = 2000
|
|
9144
|
+
cand_query += (
|
|
9145
|
+
" ORDER BY (CASE WHEN content LIKE ? THEN 0 ELSE 1 END), rowid"
|
|
9146
|
+
f" LIMIT {_FUZZY_CAND_CAP}")
|
|
9147
|
+
params_or.append(f"%{query_lower}%")
|
|
9148
|
+
|
|
9116
9149
|
def _entry_iter():
|
|
9117
9150
|
for _r in conn.execute(cand_query, params + params_or):
|
|
9118
9151
|
yield self._row_to_entry(_r)
|
|
@@ -20,10 +20,10 @@ core/mindforge.py、cli/main.py、mcp/server.py),每处都带一份硬编码
|
|
|
20
20
|
from typing import Tuple
|
|
21
21
|
|
|
22
22
|
#: 当前发布版本(唯一真值)
|
|
23
|
-
__version__: str = "6.4.
|
|
23
|
+
__version__: str = "6.4.8"
|
|
24
24
|
|
|
25
25
|
#: 结构化版本元组,便于比较(如 `VERSION_INFO >= (6, 1, 0)`)
|
|
26
|
-
VERSION_INFO: Tuple[int, int, int] = (6, 4,
|
|
26
|
+
VERSION_INFO: Tuple[int, int, int] = (6, 4, 8)
|
|
27
27
|
|
|
28
28
|
#: 下一个大版本的目标版本号(供 v6 端口预留引用)
|
|
29
29
|
NEXT_MAJOR_TARGET: str = "7.0.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mindforge-memory
|
|
3
|
-
Version: 6.4.
|
|
3
|
+
Version: 6.4.8
|
|
4
4
|
Summary: AI Agent 终身记忆系统 - 四层记忆架构 · 知识图谱 · 多模态支持
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/opok-ops/MindForge
|
|
@@ -36,11 +36,11 @@ Dynamic: license-file
|
|
|
36
36
|
|
|
37
37
|
MindForge gives AI agents a persistent, structured long-term memory: a four-tier lifecycle model, knowledge graphs, hybrid retrieval, end-to-end encryption, and MCP / CLI / REST interfaces. All data stays on your machine.
|
|
38
38
|
|
|
39
|
-
> **The story** — one developer, seven months, from v1.0 (March 2026) to v6.4.
|
|
39
|
+
> **The story** — one developer, seven months, from v1.0 (March 2026) to v6.4.8: no team, no funding, 1,252 passing tests. [Read the journey →](https://opok-ops.github.io/MindForge/#developer)
|
|
40
40
|
|
|
41
41
|
[](https://www.python.org/downloads/)
|
|
42
42
|
[](LICENSE)
|
|
43
|
-
[](https://github.com/opok-ops/MindForge/releases)
|
|
44
44
|
[](https://github.com/opok-ops/MindForge/stargazers)
|
|
45
45
|
[](https://github.com/opok-ops/MindForge/forks)
|
|
46
46
|
[](https://github.com/opok-ops/MindForge/issues)
|
|
@@ -94,10 +94,28 @@ MindForge search "what does user like"
|
|
|
94
94
|
MindForge stats
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
|
|
98
|
+
## Scale & Quality Benchmarks (v6.4.8, measured)
|
|
99
|
+
|
|
100
|
+
| Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | search p50 (pathological all-common) | db size |
|
|
101
|
+
|---|---|---|---|---|---|
|
|
102
|
+
| 200 entries (baseline) | — | 1.8 ms | — | — | <1 MB |
|
|
103
|
+
| 10,000 | 1,080/s | ~9.5 ms | ~16 ms | — | 10 MB |
|
|
104
|
+
| 100,000 | ~582/s | 6.8 ms | 135 ms | 122 ms (−44%) | 98 MB |
|
|
105
|
+
|
|
106
|
+
- Pathological queries (all-common words, synthetic template data): engine-level steady-state **4.0ms at 30k** (was 3825ms, −99.9%) — inverted-index fallback is now shortest-chain-first with a hard traversal budget; the remaining 100k cost is FTS5 high-frequency-phrase scoring (v6.5 target, honestly documented).
|
|
107
|
+
- **Index-blindness fix (v6.4.8)**: TF-IDF vocabulary was locked to the first 5 documents since v6.4.6 — incremental adds indexed as empty vectors (TF-IDF effectively half-dead; previous numbers carried by FTS5/fuzzy). Fixed with incremental vocabulary growth; all pipeline numbers above are now *true full-pipeline* measurements.
|
|
108
|
+
- Retrieval quality (fixed-seed, `benchmarks/quality_bench.py`, 2k entries): needle Recall@1 = 1.0, MRR = 1.0, topic Recall@1 = 1.0, noise false-recall = 0.
|
|
109
|
+
- Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
|
|
110
|
+
- TF-IDF degraded mode process memory at 100k: ~700 MB. Index LRU cap: 100,000 docs (MINDFORGE_INDEX_MAX_DOCS).
|
|
111
|
+
|
|
97
112
|
## What's New
|
|
98
113
|
|
|
99
|
-
v6.1.3 – v6.4.
|
|
114
|
+
v6.1.3 – v6.4.8 shipped in ten days (2026-11-01 → 11-11; three releases in the last two days), guarded by **1,253 tests** (1,252 passing, 1 environment-skipped).
|
|
115
|
+
|
|
116
|
+
- **v6.4.8 — pathological query path & index-blindness fix: O(N) fallback eliminated, TF-IDF actually indexing** (2026-11-11). Two deeper findings beyond the v6.4.6 stress test: (1) the all-common-word fallback traversed *all* inverted chains at full length (O(N×dims), 209ms p50 at 100k), and the 0.5×N max_df threshold still let half-corpus chains into the active set; (2) **the TF-IDF vocabulary had been locked to the first 5 documents** — every incremental add after that indexed as an empty vector (the TF-IDF path was effectively half-dead since v6.4.6; published numbers were carried by FTS5/fuzzy). Fix, six cuts: max_df tightened 0.5×N→**0.05×N**; fallback semantics → **shortest-chain-first** (≤3 dims, per-chain cap 5,000, islice); fuzzy candidate budget (SQL cap 2,000 with exact-substring-first ordering); fuzzy gating (runs only on zero TF-IDF hits — the true typo-correction scenario; encrypted mode unaffected); `incremental_fit()` vocabulary growth for adds after fit (old vectors not recomputed — new tokens contribute zero on old docs mathematically); empty-query fallback-signal reset. 30k repro: pathological steady-state 3825→**4.0ms** (−99.9%), mixed pipeline 1607→1.2ms, fuzzy single-path −93%. 100k re-bench: pathological −44%, all numbers now true full-pipeline. Remaining bottleneck honestly documented: FTS5 high-frequency-phrase scoring (v6.5). New observable `_last_traversed` + deterministic budget contracts. 9 new guards.
|
|
100
117
|
|
|
118
|
+
- **v6.4.6 — scale validation & retrieval benchmarks** (2026-11-10). First 100,000-entry stress test: add throughput ~600/s (linear), ~98MB db, memory peak ~700MB (TF-IDF degraded mode), search p50 7.4ms (rare token, −92% vs pre-fix) / 143ms (realistic mixed); pathological all-common query 421→218ms (−48%). Fixed a real bottleneck found by the stress test: Chinese long-query p50 was 421ms — profile showed low-IDF common words degenerating the inverted index into full-corpus scans. Fix: two-tier pruning (relative-eps + standard IR max_df stop-wording for df > 0.5×N) + heapq.nlargest; pathological all-common-word queries (synthetic-template data only) honestly documented as known O(N) characteristic, realistic queries verified unaffected (8k A/B, 30k side-by-side: 43ms mixed vs 86ms pathological). Shipped reproducible benchmarks (`benchmarks/scale_bench.py`, `benchmarks/quality_bench.py`): needle Recall@1 = 1.0 / MRR = 1.0, zero noise false-recall. Full LLM-judged benchmarks (LoCoMo/LongMemEval) deferred — needs external LLM API key. 7 new guards.
|
|
101
119
|
- **v6.4.5 — audit-driven fixes** (2026-11-10; v6.4.4 skipped by maintainer decision). Two inverse doc-code inconsistencies (v6.4.3 fixed "claimed but not done"; this one fixes "done but not tracked"): `connectors.py` outgoing HTTP User-Agent was hardcoded `MindForge-Connector/5.8.9` — lying about the version to every target site since the 5.8.9 era (`event_bus.py` fixed the same disease in v5.6.8; this one slipped through). Now dynamic from the single version truth. Website "core modules" count was stale (17 in prose, 15 in stat card, 31 measured) — both fixed to the measured 31; MCP tool count 87 verified exact, untouched. Audit archive (not a defect): WAL auto-checkpoint verified healthy under daemon-like load — 4 rounds × 400 mixed ops, WAL converged at ~4.1MB (default 1000-page threshold), TRUNCATE resets to zero. 5 new guards in `tests/test_v645_fixes.py`.
|
|
102
120
|
- **v6.4.3 — documentation-code consistency fix** (2026-11-10). Pre-release audit found that v6.4.2's claimed first cut — WAL + `synchronous=NORMAL` — **never landed in code**: `_get_conn()` set only `journal_mode=WAL`, leaving `synchronous` at SQLite's default FULL. The 2.1× search gain was real, but carried entirely by the other four cuts — real numbers, misattributed cause. This release adds `PRAGMA synchronous=NORMAL` on the WAL branch (per-connection, cross-thread; network-FS DELETE fallback stays FULL by design). Same-machine A/B on SSD shows no statistically significant latency delta (search p50 1.8 → 1.8ms — fsync is ~0.1ms-scale here); the benefit targets slow disks and IO-saturated containers, and more importantly makes the documented power-loss semantics (un-checkpointed access counts / search audits may be lost; compliance audits still commit immediately) actually hold. 4 new guard tests in `tests/test_v643_fixes.py`.
|
|
103
121
|
- **v6.4.2 — tens-of-thousands-chars latency: another 2×** (2026-11-09). Same-machine A/B benchmark (400 memories / ~47k chars, 3 runs each): **search p50 6.3→3.0ms (2.1×), p95 11.7→4.2ms (2.8×)**; 24k-char library p50 2.5ms; fast machine state 1.8ms. Five surgical cuts: WAL + `synchronous=NORMAL` (commits skip per-write fsync), enum-downgrade memo, `row.keys()` set reuse, first-visit access-count batching merged into one `flush_access()` transaction per search, and search-audit INSERT folded into the same commit. The v5.6.3 throttle contract and v5.5.4 `recently_accessed` semantics are pinned by 23 contract tests — all green. Compliance-grade audits still commit immediately. *(Note: the `synchronous=NORMAL` cut landed in v6.4.3 — see above.)*
|
|
@@ -168,7 +186,7 @@ v6.1.3 – v6.4.5 shipped in ten days (2026-11-01 → 11-10; two same-day releas
|
|
|
168
186
|
|
|
169
187
|
```
|
|
170
188
|
┌──────────────────────────────────────────────────────┐
|
|
171
|
-
│ MindForge v6.4.
|
|
189
|
+
│ MindForge v6.4.8 │
|
|
172
190
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
173
191
|
│ MemoryEvolution · FederatedMemory │
|
|
174
192
|
├──────────────────────────────────────────────────────┤
|
|
@@ -68,6 +68,7 @@ mindforge_memory.egg-info/requires.txt
|
|
|
68
68
|
mindforge_memory.egg-info/top_level.txt
|
|
69
69
|
tests/test_core.py
|
|
70
70
|
tests/test_security_hardening.py
|
|
71
|
+
tests/test_trash_hard_delete.py
|
|
71
72
|
tests/test_v546_features.py
|
|
72
73
|
tests/test_v547_fixes.py
|
|
73
74
|
tests/test_v548_features.py
|
|
@@ -133,4 +134,6 @@ tests/test_v635_integration.py
|
|
|
133
134
|
tests/test_v636_fixes.py
|
|
134
135
|
tests/test_v643_fixes.py
|
|
135
136
|
tests/test_v645_fixes.py
|
|
137
|
+
tests/test_v646_fixes.py
|
|
138
|
+
tests/test_v648_fixes.py
|
|
136
139
|
tests/test_v6_ports.py
|