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.
Files changed (141) hide show
  1. {mindforge_memory-6.4.6/mindforge_memory.egg-info → mindforge_memory-6.4.8}/PKG-INFO +15 -12
  2. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/README.md +14 -11
  3. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/indexer.py +105 -11
  4. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/query.py +26 -2
  5. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/storage.py +19 -0
  6. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/version.py +2 -2
  7. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8/mindforge_memory.egg-info}/PKG-INFO +15 -12
  8. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/SOURCES.txt +1 -0
  9. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v568_fixes.py +1 -1
  10. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v575_release_consistency.py +2 -2
  11. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v612_fixes.py +2 -2
  12. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v643_fixes.py +1 -1
  13. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v645_fixes.py +1 -1
  14. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v646_fixes.py +1 -1
  15. mindforge_memory-6.4.8/tests/test_v648_fixes.py +189 -0
  16. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/LICENSE +0 -0
  17. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/MindForge.py +0 -0
  18. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/__init__.py +0 -0
  19. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/__init__.py +0 -0
  20. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/claude_adapter.py +0 -0
  21. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/crewai_memory.py +0 -0
  22. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/generic_api.py +0 -0
  23. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/langgraph_store.py +0 -0
  24. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/adapters/openclaw_adapter.py +0 -0
  25. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/api/__init__.py +0 -0
  26. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/api/server.py +0 -0
  27. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/cli/__init__.py +0 -0
  28. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/cli/main.py +0 -0
  29. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/__init__.py +0 -0
  30. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/embedding.py +0 -0
  31. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/encryption.py +0 -0
  32. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/mindforge.py +0 -0
  33. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/paths.py +0 -0
  34. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/core/types.py +0 -0
  35. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/daemon/__init__.py +0 -0
  36. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/daemon/server.py +0 -0
  37. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/mcp/__init__.py +0 -0
  38. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/mcp/server.py +0 -0
  39. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/__init__.py +0 -0
  40. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/categorizer.py +0 -0
  41. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/conflict_detector.py +0 -0
  42. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/connectors.py +0 -0
  43. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/context_window.py +0 -0
  44. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/event_bus.py +0 -0
  45. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/evolution.py +0 -0
  46. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/experience.py +0 -0
  47. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/federated.py +0 -0
  48. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/federated_acl.py +0 -0
  49. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/follow_up.py +0 -0
  50. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/hybrid_search.py +0 -0
  51. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/importer.py +0 -0
  52. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/integrator.py +0 -0
  53. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/intent_router.py +0 -0
  54. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/knowledge_graph.py +0 -0
  55. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/memory_compressor.py +0 -0
  56. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/memory_decay.py +0 -0
  57. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/memory_passport.py +0 -0
  58. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/multi_agent.py +0 -0
  59. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/multimodal.py +0 -0
  60. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/outcome_learning.py +0 -0
  61. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/personality.py +0 -0
  62. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/principled_forgetting.py +0 -0
  63. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/privacy.py +0 -0
  64. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/recall.py +0 -0
  65. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/session_focus.py +0 -0
  66. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/share_conflict.py +0 -0
  67. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/skill_extractor.py +0 -0
  68. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/skill_pack.py +0 -0
  69. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/timeline_viz.py +0 -0
  70. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge/modules/v6.py +0 -0
  71. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/dependency_links.txt +0 -0
  72. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/entry_points.txt +0 -0
  73. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/requires.txt +0 -0
  74. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/mindforge_memory.egg-info/top_level.txt +0 -0
  75. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/pyproject.toml +0 -0
  76. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/setup.cfg +0 -0
  77. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/setup.py +0 -0
  78. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_core.py +0 -0
  79. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_security_hardening.py +0 -0
  80. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_trash_hard_delete.py +0 -0
  81. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v546_features.py +0 -0
  82. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v547_fixes.py +0 -0
  83. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v548_features.py +0 -0
  84. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v550_features.py +0 -0
  85. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v552_features.py +0 -0
  86. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v554_features.py +0 -0
  87. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v556_features.py +0 -0
  88. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v557_banner_insight_wal.py +0 -0
  89. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v557_security_fixes.py +0 -0
  90. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v557_webhook_sig_export.py +0 -0
  91. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v558_audit_fixes.py +0 -0
  92. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v558_features.py +0 -0
  93. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v560_decay_gc.py +0 -0
  94. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v560_kdf_versioning.py +0 -0
  95. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v560_module_coverage.py +0 -0
  96. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v561_security.py +0 -0
  97. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v562_p2_fixes.py +0 -0
  98. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v563_entry_coverage.py +0 -0
  99. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v563_perf_fixes.py +0 -0
  100. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v565_security.py +0 -0
  101. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v566_encrypted_search.py +0 -0
  102. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v567_security_fixes.py +0 -0
  103. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v569_fts_sync.py +0 -0
  104. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v570_coverage_expansion.py +0 -0
  105. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v570_drama_and_utils.py +0 -0
  106. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v570_encrypted_fallback.py +0 -0
  107. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v571_fixes.py +0 -0
  108. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v572_security.py +0 -0
  109. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v573_encrypted_export.py +0 -0
  110. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v573_security.py +0 -0
  111. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v576_bitemporal.py +0 -0
  112. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v576_gdpr.py +0 -0
  113. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v577_agent_governance.py +0 -0
  114. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v578_embedding.py +0 -0
  115. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v579_graph.py +0 -0
  116. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v580_experience.py +0 -0
  117. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v5812_bugfixes.py +0 -0
  118. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v583_security.py +0 -0
  119. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v584_adapters.py +0 -0
  120. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v585_benchmark.py +0 -0
  121. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v586_federated_mcp.py +0 -0
  122. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v590_features.py +0 -0
  123. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v592_features.py +0 -0
  124. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v593_features.py +0 -0
  125. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v595_features.py +0 -0
  126. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_forgetting_followup.py +0 -0
  127. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_import_timeline.py +0 -0
  128. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_multi_agent_formal.py +0 -0
  129. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_outcome_passport.py +0 -0
  130. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v600_skillpack_evolution.py +0 -0
  131. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v611_fixes.py +0 -0
  132. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v612_cascade_fixes.py +0 -0
  133. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v613_integrity.py +0 -0
  134. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v620_growth_governance.py +0 -0
  135. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v630_search_explain.py +0 -0
  136. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v631_scrypt.py +0 -0
  137. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v632_explain_interfaces.py +0 -0
  138. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v635_features.py +0 -0
  139. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v635_integration.py +0 -0
  140. {mindforge_memory-6.4.6 → mindforge_memory-6.4.8}/tests/test_v636_fixes.py +0 -0
  141. {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.6
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.5: no team, no funding, 1,243 passing tests. [Read the journey →](https://opok-ops.github.io/MindForge/#developer)
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
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
42
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
- [![Version](https://img.shields.io/badge/version-6.4.6-green.svg)](https://github.com/opok-ops/MindForge/releases)
43
+ [![Version](https://img.shields.io/badge/version-6.4.8-green.svg)](https://github.com/opok-ops/MindForge/releases)
44
44
  [![Stars](https://img.shields.io/github/stars/opok-ops/MindForge.svg)](https://github.com/opok-ops/MindForge/stargazers)
45
45
  [![Forks](https://img.shields.io/github/forks/opok-ops/MindForge.svg)](https://github.com/opok-ops/MindForge/forks)
46
46
  [![Open Issues](https://img.shields.io/github/issues/opok-ops/MindForge.svg)](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.6, measured)
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 | ~558/s | 7.4 ms | 143 ms | 98 MB |
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.5 shipped in ten days (2026-11-01 → 11-10; two same-day releases), guarded by **1,244 tests** (1,243 passing, 1 environment-skipped).
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.6 │
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.5: no team, no funding, 1,243 passing tests. [Read the journey →](https://opok-ops.github.io/MindForge/#developer)
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
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
13
- [![Version](https://img.shields.io/badge/version-6.4.6-green.svg)](https://github.com/opok-ops/MindForge/releases)
13
+ [![Version](https://img.shields.io/badge/version-6.4.8-green.svg)](https://github.com/opok-ops/MindForge/releases)
14
14
  [![Stars](https://img.shields.io/github/stars/opok-ops/MindForge.svg)](https://github.com/opok-ops/MindForge/stargazers)
15
15
  [![Forks](https://img.shields.io/github/forks/opok-ops/MindForge.svg)](https://github.com/opok-ops/MindForge/forks)
16
16
  [![Open Issues](https://img.shields.io/github/issues/opok-ops/MindForge.svg)](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.6, measured)
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 | ~558/s | 7.4 ms | 143 ms | 98 MB |
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.5 shipped in ten days (2026-11-01 → 11-10; two same-day releases), guarded by **1,244 tests** (1,243 passing, 1 environment-skipped).
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.6 │
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 停用词(df > 0.5×N
158
- # 的维是平滑 IDF 下 qval≈tf×1 的全库词——遍历整条链却无区分度,
159
- # 所有文档点积同加近似常数,top-K 排序不变;30k profile 实证
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
- _half = 0.5 * len(self.vectors)
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, ())) <= _half]
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
- active = list(q.items())
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
- return self.vector_index.search(query_vec_dict, top_k)
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
- positive = sum(1 for _, s in tfidf_results if s >= min_relevance)
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 or positive < max_results:
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.6"
23
+ __version__: str = "6.4.8"
24
24
 
25
25
  #: 结构化版本元组,便于比较(如 `VERSION_INFO >= (6, 1, 0)`)
26
- VERSION_INFO: Tuple[int, int, int] = (6, 4, 6)
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.6
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.5: no team, no funding, 1,243 passing tests. [Read the journey →](https://opok-ops.github.io/MindForge/#developer)
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
  [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
42
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
43
- [![Version](https://img.shields.io/badge/version-6.4.6-green.svg)](https://github.com/opok-ops/MindForge/releases)
43
+ [![Version](https://img.shields.io/badge/version-6.4.8-green.svg)](https://github.com/opok-ops/MindForge/releases)
44
44
  [![Stars](https://img.shields.io/github/stars/opok-ops/MindForge.svg)](https://github.com/opok-ops/MindForge/stargazers)
45
45
  [![Forks](https://img.shields.io/github/forks/opok-ops/MindForge.svg)](https://github.com/opok-ops/MindForge/forks)
46
46
  [![Open Issues](https://img.shields.io/github/issues/opok-ops/MindForge.svg)](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.6, measured)
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 | ~558/s | 7.4 ms | 143 ms | 98 MB |
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.5 shipped in ten days (2026-11-01 → 11-10; two same-day releases), guarded by **1,244 tests** (1,243 passing, 1 environment-skipped).
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.6 │
189
+ │ MindForge v6.4.8 │
187
190
  │ Cognitive Layer Personality · KnowledgeGraph │
188
191
  │ MemoryEvolution · FederatedMemory │
189
192
  ├──────────────────────────────────────────────────────┤
@@ -135,4 +135,5 @@ tests/test_v636_fixes.py
135
135
  tests/test_v643_fixes.py
136
136
  tests/test_v645_fixes.py
137
137
  tests/test_v646_fixes.py
138
+ tests/test_v648_fixes.py
138
139
  tests/test_v6_ports.py
@@ -39,7 +39,7 @@ import mindforge.core.version as core_version
39
39
 
40
40
 
41
41
  _REPO = Path(__file__).resolve().parent.parent
42
- _EXPECTED_VERSION = "6.4.6"
42
+ _EXPECTED_VERSION = "6.4.8"
43
43
 
44
44
  # 本轮修复涉及的、必须与 mindforge/core/version.py 保持一致的源码文件
45
45
  _VERSION_TRUTH_FILES = (
@@ -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.6")
44
- self.assertEqual(core_version.VERSION_INFO, (6, 4, 6))
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.6")
666
- self.assertEqual(VERSION_INFO, (6, 4, 6))
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 兜底)。"""
@@ -94,7 +94,7 @@ class TestVersionBump(unittest.TestCase):
94
94
  钉点集合随版本数递增,发版链清单必须扫全部 test_v*fixes.py。"""
95
95
 
96
96
  def test_version_is_current(self):
97
- self.assertEqual(__version__, "6.4.6")
97
+ self.assertEqual(__version__, "6.4.8")
98
98
 
99
99
 
100
100
  if __name__ == "__main__":
@@ -101,7 +101,7 @@ class TestVersionBump(unittest.TestCase):
101
101
  """版本钉值:忘记 bump 时在此显形。"""
102
102
 
103
103
  def test_version_is_current(self):
104
- self.assertEqual(__version__, "6.4.6")
104
+ self.assertEqual(__version__, "6.4.8")
105
105
 
106
106
 
107
107
  if __name__ == "__main__":
@@ -85,7 +85,7 @@ class TestVersionBump(unittest.TestCase):
85
85
  """版本钉值(v6.4.5 教训:每版 fixes 断言随发版一并 bump)。"""
86
86
 
87
87
  def test_version_is_646(self):
88
- self.assertEqual(__version__, "6.4.6")
88
+ self.assertEqual(__version__, "6.4.8")
89
89
 
90
90
 
91
91
  if __name__ == "__main__":