mindforge-memory 6.4.6__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.6/mindforge_memory.egg-info → mindforge_memory-6.4.8}/PKG-INFO +15 -12
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/README.md +14 -11
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/indexer.py +105 -11
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/query.py +26 -2
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/storage.py +19 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/version.py +2 -2
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8/mindforge_memory.egg-info}/PKG-INFO +15 -12
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/SOURCES.txt +1 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v568_fixes.py +1 -1
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v575_release_consistency.py +2 -2
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v612_fixes.py +2 -2
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v643_fixes.py +1 -1
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v645_fixes.py +1 -1
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v646_fixes.py +1 -1
- mindforge_memory-6.4.8/tests/test_v648_fixes.py +189 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/LICENSE +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/MindForge.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/claude_adapter.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/crewai_memory.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/generic_api.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/langgraph_store.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/openclaw_adapter.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/api/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/api/server.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/cli/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/cli/main.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/embedding.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/encryption.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/mindforge.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/paths.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/types.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/daemon/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/daemon/server.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/mcp/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/mcp/server.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/__init__.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/categorizer.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/conflict_detector.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/connectors.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/context_window.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/event_bus.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/evolution.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/experience.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/federated.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/federated_acl.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/follow_up.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/hybrid_search.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/importer.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/integrator.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/intent_router.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/knowledge_graph.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/memory_compressor.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/memory_decay.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/memory_passport.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/multi_agent.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/multimodal.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/outcome_learning.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/personality.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/principled_forgetting.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/privacy.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/recall.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/session_focus.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/share_conflict.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/skill_extractor.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/skill_pack.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/timeline_viz.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/v6.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/dependency_links.txt +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/entry_points.txt +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/requires.txt +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/top_level.txt +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/pyproject.toml +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/setup.cfg +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/setup.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_core.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_security_hardening.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_trash_hard_delete.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v546_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v547_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v548_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v550_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v552_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v554_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v556_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v557_banner_insight_wal.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v557_security_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v557_webhook_sig_export.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v558_audit_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v558_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v560_decay_gc.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v560_kdf_versioning.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v560_module_coverage.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v561_security.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v562_p2_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v563_entry_coverage.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v563_perf_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v565_security.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v566_encrypted_search.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v567_security_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v569_fts_sync.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v570_coverage_expansion.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v570_drama_and_utils.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v570_encrypted_fallback.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v571_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v572_security.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v573_encrypted_export.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v573_security.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v576_bitemporal.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v576_gdpr.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v577_agent_governance.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v578_embedding.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v579_graph.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v580_experience.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v5812_bugfixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v583_security.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v584_adapters.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v585_benchmark.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v586_federated_mcp.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v590_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v592_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v593_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v595_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_forgetting_followup.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_import_timeline.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_multi_agent_formal.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_outcome_passport.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_skillpack_evolution.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v611_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v612_cascade_fixes.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v613_integrity.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v620_growth_governance.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v630_search_explain.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v631_scrypt.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v632_explain_interfaces.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v635_features.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v635_integration.py +0 -0
- {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v636_fixes.py +0 -0
- {mindforge_memory-6.4.6 → 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)
|
|
@@ -95,22 +95,25 @@ MindForge stats
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
## Scale & Quality Benchmarks (v6.4.
|
|
98
|
+
## Scale & Quality Benchmarks (v6.4.8, measured)
|
|
99
99
|
|
|
100
|
-
| Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | 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 | ~
|
|
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
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.
|
|
106
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.
|
|
107
|
-
- Known characteristic: all-common-word queries on synthetic template data fall back to O(N) scan (documented, not a defect); realistic queries hit the pruned inverted index.
|
|
108
109
|
- Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
|
|
109
110
|
- TF-IDF degraded mode process memory at 100k: ~700 MB. Index LRU cap: 100,000 docs (MINDFORGE_INDEX_MAX_DOCS).
|
|
110
111
|
|
|
111
112
|
## What's New
|
|
112
113
|
|
|
113
|
-
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.
|
|
114
117
|
|
|
115
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.
|
|
116
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`.
|
|
@@ -183,7 +186,7 @@ v6.1.3 – v6.4.5 shipped in ten days (2026-11-01 → 11-10; two same-day releas
|
|
|
183
186
|
|
|
184
187
|
```
|
|
185
188
|
┌──────────────────────────────────────────────────────┐
|
|
186
|
-
│ MindForge v6.4.
|
|
189
|
+
│ MindForge v6.4.8 │
|
|
187
190
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
188
191
|
│ MemoryEvolution · FederatedMemory │
|
|
189
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)
|
|
@@ -65,22 +65,25 @@ MindForge stats
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
## Scale & Quality Benchmarks (v6.4.
|
|
68
|
+
## Scale & Quality Benchmarks (v6.4.8, measured)
|
|
69
69
|
|
|
70
|
-
| Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | 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 | ~
|
|
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
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.
|
|
76
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.
|
|
77
|
-
- Known characteristic: all-common-word queries on synthetic template data fall back to O(N) scan (documented, not a defect); realistic queries hit the pruned inverted index.
|
|
78
79
|
- Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
|
|
79
80
|
- TF-IDF degraded mode process memory at 100k: ~700 MB. Index LRU cap: 100,000 docs (MINDFORGE_INDEX_MAX_DOCS).
|
|
80
81
|
|
|
81
82
|
## What's New
|
|
82
83
|
|
|
83
|
-
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.
|
|
84
87
|
|
|
85
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.
|
|
86
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`.
|
|
@@ -153,7 +156,7 @@ v6.1.3 – v6.4.5 shipped in ten days (2026-11-01 → 11-10; two same-day releas
|
|
|
153
156
|
|
|
154
157
|
```
|
|
155
158
|
┌──────────────────────────────────────────────────────┐
|
|
156
|
-
│ MindForge v6.4.
|
|
159
|
+
│ MindForge v6.4.8 │
|
|
157
160
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
158
161
|
│ MemoryEvolution · FederatedMemory │
|
|
159
162
|
├──────────────────────────────────────────────────────┤
|
|
@@ -9,6 +9,7 @@ import os
|
|
|
9
9
|
import re
|
|
10
10
|
import sqlite3
|
|
11
11
|
from collections import Counter, defaultdict
|
|
12
|
+
from itertools import islice
|
|
12
13
|
from typing import List, Dict, Tuple, Optional
|
|
13
14
|
from dataclasses import dataclass
|
|
14
15
|
|
|
@@ -28,6 +29,7 @@ class TFIDFVectorizer:
|
|
|
28
29
|
def __init__(self):
|
|
29
30
|
self.vocab: Dict[str, int] = {}
|
|
30
31
|
self.idf: Dict[str, float] = {}
|
|
32
|
+
self._df: Dict[str, int] = {} # v6.4.8: 增量 fit 的精确 df 延续
|
|
31
33
|
self.doc_count: int = 0
|
|
32
34
|
|
|
33
35
|
def _tokenize(self, text: str) -> List[str]:
|
|
@@ -59,6 +61,33 @@ class TFIDFVectorizer:
|
|
|
59
61
|
|
|
60
62
|
for word, idx in self.vocab.items():
|
|
61
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
|
|
62
91
|
|
|
63
92
|
def transform(self, text: str) -> Dict[int, float]:
|
|
64
93
|
tokens = self._tokenize(text)
|
|
@@ -108,6 +137,10 @@ class VectorIndex:
|
|
|
108
137
|
self.metadata: Dict[str, Dict] = {}
|
|
109
138
|
self._norms: Dict[str, float] = {} # 预计算 L2 范数缓存
|
|
110
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
|
|
111
144
|
|
|
112
145
|
@staticmethod
|
|
113
146
|
def _as_sparse(vector) -> Dict[int, float]:
|
|
@@ -150,31 +183,75 @@ class VectorIndex:
|
|
|
150
183
|
q = self._as_sparse(query_vector)
|
|
151
184
|
query_norm = math.sqrt(sum(v * v for v in q.values()))
|
|
152
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
|
|
153
192
|
return [(doc_id, 0.0) for doc_id in list(self.vectors.keys())[:top_k]]
|
|
154
193
|
|
|
155
194
|
# v6.4.6 perf: 低区分度查询维剪枝。100k 规模压测发现中文长查询
|
|
156
195
|
# p50 421ms,profile 定位到常见词(如「条目/陈述性/记忆」)的倒排
|
|
157
|
-
# 链全量遍历。两层剪枝:eps 相对阈值 + max_df
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
# eps
|
|
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,遍历毫秒级。
|
|
161
202
|
_EPS_REL = 0.02
|
|
162
|
-
|
|
203
|
+
_max_df_len = 0.05 * len(self.vectors)
|
|
163
204
|
active = [(idx, qval) for idx, qval in q.items()
|
|
164
205
|
if qval > _EPS_REL * query_norm
|
|
165
|
-
and len(self._postings.get(idx, ())) <=
|
|
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
|
|
166
212
|
if not active:
|
|
167
|
-
|
|
168
|
-
|
|
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)
|
|
169
239
|
|
|
170
240
|
# 倒排链累加点积:只访问与查询共享非零维度的候选文档
|
|
241
|
+
# (active 链长上界 _max_df_len,遍历量与库容解耦)
|
|
171
242
|
dots: Dict[str, float] = {}
|
|
172
243
|
for idx, qval in active:
|
|
173
244
|
for doc_id, dval in self._postings.get(idx, {}).items():
|
|
174
245
|
dots[doc_id] = dots.get(doc_id, 0.0) + qval * dval
|
|
246
|
+
self._last_traversed += 1
|
|
175
247
|
|
|
176
248
|
# v6.4.6 perf: heapq.nlargest 替代全量 sort——候选集大时
|
|
177
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 抽出,回退/正常路径共用)"""
|
|
178
255
|
return heapq.nlargest(
|
|
179
256
|
top_k,
|
|
180
257
|
((doc_id, dot / (query_norm * self._norms[doc_id]))
|
|
@@ -212,6 +289,8 @@ class IndexEngine:
|
|
|
212
289
|
self.db_path = db_path
|
|
213
290
|
self.vectorizer = TFIDFVectorizer()
|
|
214
291
|
self.vector_index = VectorIndex()
|
|
292
|
+
# v6.4.8: 病态回退信号门面透传(见 search())
|
|
293
|
+
self.last_fallback = False
|
|
215
294
|
self._fitted = False
|
|
216
295
|
self._hydrated = False
|
|
217
296
|
self._doc_texts: Dict[str, str] = {}
|
|
@@ -269,12 +348,21 @@ class IndexEngine:
|
|
|
269
348
|
"""索引记忆"""
|
|
270
349
|
self._doc_texts[doc_id] = text
|
|
271
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 入索引。
|
|
272
360
|
if not self._fitted and len(self._doc_texts) >= 5:
|
|
273
361
|
self._fit_vectorizer()
|
|
362
|
+
elif self._fitted:
|
|
363
|
+
self.vectorizer.incremental_fit(text)
|
|
274
364
|
|
|
275
365
|
if self._fitted:
|
|
276
|
-
# v5.6.3 性能修复:直接存稀疏向量({dim: weight}),不再展开为
|
|
277
|
-
# 词表长度的稠密 list
|
|
278
366
|
vector_dict = self.vectorizer.transform(text)
|
|
279
367
|
self.vector_index.add(doc_id, vector_dict, metadata)
|
|
280
368
|
|
|
@@ -292,11 +380,17 @@ class IndexEngine:
|
|
|
292
380
|
self._fit_vectorizer()
|
|
293
381
|
|
|
294
382
|
if not self._fitted:
|
|
383
|
+
# v6.4.8: 降级路径同样透传病态回退信号(保持门面契约一致)
|
|
384
|
+
self.last_fallback = False
|
|
295
385
|
return self._keyword_search(query, top_k)
|
|
296
386
|
|
|
297
387
|
# v5.6.3 性能修复:查询向量同样保持稀疏,走倒排链检索
|
|
298
388
|
query_vec_dict = self.vectorizer.transform(query)
|
|
299
|
-
|
|
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
|
|
300
394
|
|
|
301
395
|
def _keyword_search(self, query: str, top_k: int) -> List[Tuple[str, float]]:
|
|
302
396
|
"""关键词搜索(降级方案)"""
|
|
@@ -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"
|
|
@@ -9127,6 +9127,25 @@ class StorageEngine:
|
|
|
9127
9127
|
params_or.append(f"%{_ch}%")
|
|
9128
9128
|
cand_query = base_query + " AND (" + " OR ".join(conds) + ")"
|
|
9129
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
|
+
|
|
9130
9149
|
def _entry_iter():
|
|
9131
9150
|
for _r in conn.execute(cand_query, params + params_or):
|
|
9132
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)
|
|
@@ -95,22 +95,25 @@ MindForge stats
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
## Scale & Quality Benchmarks (v6.4.
|
|
98
|
+
## Scale & Quality Benchmarks (v6.4.8, measured)
|
|
99
99
|
|
|
100
|
-
| Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | 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 | ~
|
|
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
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.
|
|
106
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.
|
|
107
|
-
- Known characteristic: all-common-word queries on synthetic template data fall back to O(N) scan (documented, not a defect); realistic queries hit the pruned inverted index.
|
|
108
109
|
- Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
|
|
109
110
|
- TF-IDF degraded mode process memory at 100k: ~700 MB. Index LRU cap: 100,000 docs (MINDFORGE_INDEX_MAX_DOCS).
|
|
110
111
|
|
|
111
112
|
## What's New
|
|
112
113
|
|
|
113
|
-
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.
|
|
114
117
|
|
|
115
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.
|
|
116
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`.
|
|
@@ -183,7 +186,7 @@ v6.1.3 – v6.4.5 shipped in ten days (2026-11-01 → 11-10; two same-day releas
|
|
|
183
186
|
|
|
184
187
|
```
|
|
185
188
|
┌──────────────────────────────────────────────────────┐
|
|
186
|
-
│ MindForge v6.4.
|
|
189
|
+
│ MindForge v6.4.8 │
|
|
187
190
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
188
191
|
│ MemoryEvolution · FederatedMemory │
|
|
189
192
|
├──────────────────────────────────────────────────────┤
|
|
@@ -40,8 +40,8 @@ class TestVersionTruth(unittest.TestCase):
|
|
|
40
40
|
def test_core_version_matches_release(self):
|
|
41
41
|
# 版本推进时必须一并更新此处的钉值(与 test_v568_fixes 的
|
|
42
42
|
# _EXPECTED_VERSION 同一约定:让「忘记 bump 版本」在测试里显形)。
|
|
43
|
-
self.assertEqual(core_version.__version__, "6.4.
|
|
44
|
-
self.assertEqual(core_version.VERSION_INFO, (6, 4,
|
|
43
|
+
self.assertEqual(core_version.__version__, "6.4.8")
|
|
44
|
+
self.assertEqual(core_version.VERSION_INFO, (6, 4, 8))
|
|
45
45
|
|
|
46
46
|
def test_pyproject_matches_core_version(self):
|
|
47
47
|
# v5.8.13 起 pyproject 不再写死静态版本:
|
|
@@ -662,8 +662,8 @@ class TestVersionConsistency(unittest.TestCase):
|
|
|
662
662
|
def test_version_is_612(self):
|
|
663
663
|
from mindforge.core.version import VERSION_INFO, __version__
|
|
664
664
|
# v6.1.3:版本钉值随发版推进更新(历史方法名保留)
|
|
665
|
-
self.assertEqual(__version__, "6.4.
|
|
666
|
-
self.assertEqual(VERSION_INFO, (6, 4,
|
|
665
|
+
self.assertEqual(__version__, "6.4.8")
|
|
666
|
+
self.assertEqual(VERSION_INFO, (6, 4, 8))
|
|
667
667
|
|
|
668
668
|
def test_json_round_trip_safety_of_preview(self):
|
|
669
669
|
"""save_evolution 的预览序列化不得抛(不可 json 化的对象走 str 兜底)。"""
|