mindforge-memory 6.4.8__tar.gz → 6.4.9__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 (142) hide show
  1. {mindforge_memory-6.4.8/mindforge_memory.egg-info → mindforge_memory-6.4.9}/PKG-INFO +10 -8
  2. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/README.md +9 -7
  3. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/indexer.py +81 -22
  4. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/query.py +27 -37
  5. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/storage.py +4 -13
  6. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/version.py +2 -2
  7. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9/mindforge_memory.egg-info}/PKG-INFO +10 -8
  8. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/SOURCES.txt +1 -0
  9. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v568_fixes.py +1 -1
  10. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v575_release_consistency.py +2 -2
  11. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v612_fixes.py +2 -2
  12. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v643_fixes.py +1 -1
  13. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v645_fixes.py +1 -1
  14. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v646_fixes.py +1 -1
  15. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v648_fixes.py +5 -3
  16. mindforge_memory-6.4.9/tests/test_v649_fixes.py +126 -0
  17. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/LICENSE +0 -0
  18. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/MindForge.py +0 -0
  19. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/__init__.py +0 -0
  20. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/__init__.py +0 -0
  21. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/claude_adapter.py +0 -0
  22. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/crewai_memory.py +0 -0
  23. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/generic_api.py +0 -0
  24. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/langgraph_store.py +0 -0
  25. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/openclaw_adapter.py +0 -0
  26. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/api/__init__.py +0 -0
  27. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/api/server.py +0 -0
  28. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/cli/__init__.py +0 -0
  29. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/cli/main.py +0 -0
  30. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/__init__.py +0 -0
  31. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/embedding.py +0 -0
  32. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/encryption.py +0 -0
  33. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/mindforge.py +0 -0
  34. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/paths.py +0 -0
  35. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/types.py +0 -0
  36. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/daemon/__init__.py +0 -0
  37. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/daemon/server.py +0 -0
  38. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/mcp/__init__.py +0 -0
  39. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/mcp/server.py +0 -0
  40. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/__init__.py +0 -0
  41. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/categorizer.py +0 -0
  42. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/conflict_detector.py +0 -0
  43. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/connectors.py +0 -0
  44. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/context_window.py +0 -0
  45. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/event_bus.py +0 -0
  46. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/evolution.py +0 -0
  47. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/experience.py +0 -0
  48. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/federated.py +0 -0
  49. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/federated_acl.py +0 -0
  50. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/follow_up.py +0 -0
  51. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/hybrid_search.py +0 -0
  52. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/importer.py +0 -0
  53. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/integrator.py +0 -0
  54. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/intent_router.py +0 -0
  55. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/knowledge_graph.py +0 -0
  56. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/memory_compressor.py +0 -0
  57. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/memory_decay.py +0 -0
  58. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/memory_passport.py +0 -0
  59. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/multi_agent.py +0 -0
  60. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/multimodal.py +0 -0
  61. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/outcome_learning.py +0 -0
  62. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/personality.py +0 -0
  63. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/principled_forgetting.py +0 -0
  64. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/privacy.py +0 -0
  65. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/recall.py +0 -0
  66. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/session_focus.py +0 -0
  67. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/share_conflict.py +0 -0
  68. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/skill_extractor.py +0 -0
  69. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/skill_pack.py +0 -0
  70. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/timeline_viz.py +0 -0
  71. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/v6.py +0 -0
  72. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/dependency_links.txt +0 -0
  73. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/entry_points.txt +0 -0
  74. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/requires.txt +0 -0
  75. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/top_level.txt +0 -0
  76. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/pyproject.toml +0 -0
  77. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/setup.cfg +0 -0
  78. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/setup.py +0 -0
  79. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_core.py +0 -0
  80. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_security_hardening.py +0 -0
  81. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_trash_hard_delete.py +0 -0
  82. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v546_features.py +0 -0
  83. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v547_fixes.py +0 -0
  84. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v548_features.py +0 -0
  85. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v550_features.py +0 -0
  86. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v552_features.py +0 -0
  87. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v554_features.py +0 -0
  88. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v556_features.py +0 -0
  89. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v557_banner_insight_wal.py +0 -0
  90. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v557_security_fixes.py +0 -0
  91. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v557_webhook_sig_export.py +0 -0
  92. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v558_audit_fixes.py +0 -0
  93. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v558_features.py +0 -0
  94. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v560_decay_gc.py +0 -0
  95. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v560_kdf_versioning.py +0 -0
  96. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v560_module_coverage.py +0 -0
  97. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v561_security.py +0 -0
  98. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v562_p2_fixes.py +0 -0
  99. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v563_entry_coverage.py +0 -0
  100. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v563_perf_fixes.py +0 -0
  101. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v565_security.py +0 -0
  102. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v566_encrypted_search.py +0 -0
  103. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v567_security_fixes.py +0 -0
  104. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v569_fts_sync.py +0 -0
  105. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v570_coverage_expansion.py +0 -0
  106. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v570_drama_and_utils.py +0 -0
  107. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v570_encrypted_fallback.py +0 -0
  108. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v571_fixes.py +0 -0
  109. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v572_security.py +0 -0
  110. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v573_encrypted_export.py +0 -0
  111. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v573_security.py +0 -0
  112. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v576_bitemporal.py +0 -0
  113. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v576_gdpr.py +0 -0
  114. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v577_agent_governance.py +0 -0
  115. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v578_embedding.py +0 -0
  116. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v579_graph.py +0 -0
  117. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v580_experience.py +0 -0
  118. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v5812_bugfixes.py +0 -0
  119. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v583_security.py +0 -0
  120. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v584_adapters.py +0 -0
  121. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v585_benchmark.py +0 -0
  122. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v586_federated_mcp.py +0 -0
  123. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v590_features.py +0 -0
  124. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v592_features.py +0 -0
  125. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v593_features.py +0 -0
  126. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v595_features.py +0 -0
  127. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_forgetting_followup.py +0 -0
  128. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_import_timeline.py +0 -0
  129. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_multi_agent_formal.py +0 -0
  130. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_outcome_passport.py +0 -0
  131. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_skillpack_evolution.py +0 -0
  132. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v611_fixes.py +0 -0
  133. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v612_cascade_fixes.py +0 -0
  134. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v613_integrity.py +0 -0
  135. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v620_growth_governance.py +0 -0
  136. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v630_search_explain.py +0 -0
  137. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v631_scrypt.py +0 -0
  138. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v632_explain_interfaces.py +0 -0
  139. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v635_features.py +0 -0
  140. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v635_integration.py +0 -0
  141. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v636_fixes.py +0 -0
  142. {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/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.8
3
+ Version: 6.4.9
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.8: no team, no funding, 1,252 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.9: no team, no funding, 1,257 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.8-green.svg)](https://github.com/opok-ops/MindForge/releases)
43
+ [![Version](https://img.shields.io/badge/version-6.4.9-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,15 +95,15 @@ MindForge stats
95
95
  ```
96
96
 
97
97
 
98
- ## Scale & Quality Benchmarks (v6.4.8, measured)
98
+ ## Scale & Quality Benchmarks (v6.4.9, measured)
99
99
 
100
100
  | Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | search p50 (pathological all-common) | db size |
101
101
  |---|---|---|---|---|---|
102
102
  | 200 entries (baseline) | — | 1.8 ms | — | — | <1 MB |
103
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 |
104
+ | 100,000 (~10M chars) | ~546/s | 6.8 ms | 3.0 ms | 0.4 ms | 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).
106
+ - **Ten-million-character library, millisecond-class across all query types (v6.4.9)**: pathological 218→**0.4ms** (−99.8%), mixed 143→**3.0ms** (−98%), rare 6.8ms, combined 17.8ms (100k full-pipeline, clean environment). The last bottleneck — FTS5 high-frequency-phrase bm25 scoring — is now pruned query-side via trigram-df pre-check (df upper bound = min trigram df, pruned at 5% corpus size, symmetric to the TF-IDF max_df design; df served from an in-process cache — fts5vocab is a streaming virtual table, per-term probes scan it fully).
107
107
  - **Index-blindness fix (v6.4.8)**: TF-IDF vocabulary was locked to the first 5 documents since v6.4.6 — incremental adds indexed as empty vectors (TF-IDF effectively half-dead; previous numbers carried by FTS5/fuzzy). Fixed with incremental vocabulary growth; all pipeline numbers above are now *true full-pipeline* measurements.
108
108
  - Retrieval quality (fixed-seed, `benchmarks/quality_bench.py`, 2k entries): needle Recall@1 = 1.0, MRR = 1.0, topic Recall@1 = 1.0, noise false-recall = 0.
109
109
  - Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
@@ -111,7 +111,9 @@ MindForge stats
111
111
 
112
112
  ## What's New
113
113
 
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).
114
+ v6.1.3 – v6.4.9 shipped in ten days (2026-11-01 → 11-11; four releases in the last two days), guarded by **1,258 tests** (1,257 passing, 1 environment-skipped).
115
+
116
+ - **v6.4.9 — ten-million-char library at millisecond latency: FTS5 high-frequency-phrase scoring pruned** (2026-11-11). The last honest bottleneck from v6.4.8's ledger. Root cause (30k per-path timing): under the trigram tokenizer, corpus-wide phrases (template data's "陈述性记忆") match every row — bm25 full-match scoring cost 50.5ms/query, the dominant remainder of 100k mixed latency. Fix, query-side df pre-pruning symmetric to TF-IDF max_df: a phrase's df upper bound = min df of its trigrams (phrase matches ⊆ every trigram's row set); prune at 5% of corpus size; keep the lowest-ub phrase if all pruned. df is served from an in-process cache keyed by `max(rowid)` (add/update invalidate; hard-delete leaves df slightly stale = prunes more conservatively) — **two real pitfalls found en route: fts5vocab is a *streaming virtual table* — per-term probes AND `IN` batches trigger full scans (combined regressed 17→45ms before the cache)**. Plus: FTS5 path skipped on pathological fallback (no discriminating signal). Clean-environment 100k re-bench: pathological 122→**0.4ms** (−99.7%), mixed 134.9→**3.0ms** (−98%), all query types millisecond-class at ~10M chars. AI-flavored comment cleanup (essay-style docstrings → engineer-speak, info preserved). 5 new guards.
115
117
 
116
118
  - **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.
117
119
 
@@ -186,7 +188,7 @@ v6.1.3 – v6.4.8 shipped in ten days (2026-11-01 → 11-11; three releases in t
186
188
 
187
189
  ```
188
190
  ┌──────────────────────────────────────────────────────┐
189
- │ MindForge v6.4.8 │
191
+ │ MindForge v6.4.9 │
190
192
  │ Cognitive Layer Personality · KnowledgeGraph │
191
193
  │ MemoryEvolution · FederatedMemory │
192
194
  ├──────────────────────────────────────────────────────┤
@@ -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.8: no team, no funding, 1,252 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.9: no team, no funding, 1,257 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.8-green.svg)](https://github.com/opok-ops/MindForge/releases)
13
+ [![Version](https://img.shields.io/badge/version-6.4.9-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,15 +65,15 @@ MindForge stats
65
65
  ```
66
66
 
67
67
 
68
- ## Scale & Quality Benchmarks (v6.4.8, measured)
68
+ ## Scale & Quality Benchmarks (v6.4.9, measured)
69
69
 
70
70
  | Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | search p50 (pathological all-common) | db size |
71
71
  |---|---|---|---|---|---|
72
72
  | 200 entries (baseline) | — | 1.8 ms | — | — | <1 MB |
73
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 |
74
+ | 100,000 (~10M chars) | ~546/s | 6.8 ms | 3.0 ms | 0.4 ms | 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).
76
+ - **Ten-million-character library, millisecond-class across all query types (v6.4.9)**: pathological 218→**0.4ms** (−99.8%), mixed 143→**3.0ms** (−98%), rare 6.8ms, combined 17.8ms (100k full-pipeline, clean environment). The last bottleneck — FTS5 high-frequency-phrase bm25 scoring — is now pruned query-side via trigram-df pre-check (df upper bound = min trigram df, pruned at 5% corpus size, symmetric to the TF-IDF max_df design; df served from an in-process cache — fts5vocab is a streaming virtual table, per-term probes scan it fully).
77
77
  - **Index-blindness fix (v6.4.8)**: TF-IDF vocabulary was locked to the first 5 documents since v6.4.6 — incremental adds indexed as empty vectors (TF-IDF effectively half-dead; previous numbers carried by FTS5/fuzzy). Fixed with incremental vocabulary growth; all pipeline numbers above are now *true full-pipeline* measurements.
78
78
  - Retrieval quality (fixed-seed, `benchmarks/quality_bench.py`, 2k entries): needle Recall@1 = 1.0, MRR = 1.0, topic Recall@1 = 1.0, noise false-recall = 0.
79
79
  - Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
@@ -81,7 +81,9 @@ MindForge stats
81
81
 
82
82
  ## What's New
83
83
 
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).
84
+ v6.1.3 – v6.4.9 shipped in ten days (2026-11-01 → 11-11; four releases in the last two days), guarded by **1,258 tests** (1,257 passing, 1 environment-skipped).
85
+
86
+ - **v6.4.9 — ten-million-char library at millisecond latency: FTS5 high-frequency-phrase scoring pruned** (2026-11-11). The last honest bottleneck from v6.4.8's ledger. Root cause (30k per-path timing): under the trigram tokenizer, corpus-wide phrases (template data's "陈述性记忆") match every row — bm25 full-match scoring cost 50.5ms/query, the dominant remainder of 100k mixed latency. Fix, query-side df pre-pruning symmetric to TF-IDF max_df: a phrase's df upper bound = min df of its trigrams (phrase matches ⊆ every trigram's row set); prune at 5% of corpus size; keep the lowest-ub phrase if all pruned. df is served from an in-process cache keyed by `max(rowid)` (add/update invalidate; hard-delete leaves df slightly stale = prunes more conservatively) — **two real pitfalls found en route: fts5vocab is a *streaming virtual table* — per-term probes AND `IN` batches trigger full scans (combined regressed 17→45ms before the cache)**. Plus: FTS5 path skipped on pathological fallback (no discriminating signal). Clean-environment 100k re-bench: pathological 122→**0.4ms** (−99.7%), mixed 134.9→**3.0ms** (−98%), all query types millisecond-class at ~10M chars. AI-flavored comment cleanup (essay-style docstrings → engineer-speak, info preserved). 5 new guards.
85
87
 
86
88
  - **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.
87
89
 
@@ -156,7 +158,7 @@ v6.1.3 – v6.4.8 shipped in ten days (2026-11-01 → 11-11; three releases in t
156
158
 
157
159
  ```
158
160
  ┌──────────────────────────────────────────────────────┐
159
- │ MindForge v6.4.8 │
161
+ │ MindForge v6.4.9 │
160
162
  │ Cognitive Layer Personality · KnowledgeGraph │
161
163
  │ MemoryEvolution · FederatedMemory │
162
164
  ├──────────────────────────────────────────────────────┤
@@ -205,20 +205,15 @@ class VectorIndex:
205
205
  if qval > _EPS_REL * query_norm
206
206
  and len(self._postings.get(idx, ())) <= _max_df_len]
207
207
  self._last_traversed = 0
208
- # v6.4.8: 病态回退信号——查询全部维度超 max_df(全库词)时
209
- # 置位,供上层 QueryEngine 跳过 fuzzy 兜底路(全库词查询对
210
- # fuzzy 的边际价值为零,详见 query.py 路 2 注释)。
208
+ # v6.4.8: 全库词回退信号(供 query.py 跳过 fuzzy/FTS5 兜底)
211
209
  self.last_fallback = False
212
210
  if not active:
213
211
  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 语义,优先守住延迟上界
212
+ # v6.4.8: 回退不再遍历全部倒排链(旧 O(N×dims) 是 100k
213
+ # 全库词查询 209ms 的根因)。全库词点积贡献是常数,top-K
214
+ # 只由相对区分度决定:取最短 _FALLBACK_DIMS 维(区分度最高
215
+ # 的信号),单链截断 _TRAVERSE_CAP 项(平分数下截断不改
216
+ # top-K 语义)。
222
217
  _FALLBACK_DIMS = 3
223
218
  _TRAVERSE_CAP = 5000
224
219
  ranked = sorted(
@@ -291,6 +286,9 @@ class IndexEngine:
291
286
  self.vector_index = VectorIndex()
292
287
  # v6.4.8: 病态回退信号门面透传(见 search())
293
288
  self.last_fallback = False
289
+ # v6.4.9: FTS5 trigram df 进程内缓存(见 _prune_fts_query)
290
+ self._fts_vocab_cache: Optional[Dict[str, int]] = None
291
+ self._fts_vocab_rowid: int = -1
294
292
  self._fitted = False
295
293
  self._hydrated = False
296
294
  self._doc_texts: Dict[str, str] = {}
@@ -348,15 +346,10 @@ class IndexEngine:
348
346
  """索引记忆"""
349
347
  self._doc_texts[doc_id] = text
350
348
 
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 入索引。
349
+ # v5.6.5 P1 #5: 渐进 fit 触发点(LRU 守卫契约:add 即索引)。
350
+ # v6.4.8 fix: fit 后的增量 add 先 incremental_fit 扩词表再
351
+ # transform——旧逻辑词表锁死在前 5 条文档,此后增量 add 的
352
+ # transform 全 vocab miss(空向量入索引,TF-IDF 路半瘫痪)。
360
353
  if not self._fitted and len(self._doc_texts) >= 5:
361
354
  self._fit_vectorizer()
362
355
  elif self._fitted:
@@ -366,7 +359,7 @@ class IndexEngine:
366
359
  vector_dict = self.vectorizer.transform(text)
367
360
  self.vector_index.add(doc_id, vector_dict, metadata)
368
361
 
369
- # v5.6.5 P1 #5:写入后做 LRU 淘汰(覆盖写不增加条目数,故在 add 之后)
362
+ # v5.6.5 P1 #5: 写入后 LRU 淘汰
370
363
  self._evict_if_needed()
371
364
 
372
365
  def remove_memory(self, doc_id: str):
@@ -446,9 +439,16 @@ class IndexEngine:
446
439
 
447
440
  def fts_search(self, conn: sqlite3.Connection, query: str,
448
441
  top_k: int = 10) -> List[Tuple[str, float]]:
449
- """FTS5 全文搜索(v5.4.7 修复:特殊字符转义)"""
442
+ """FTS5 全文搜索(v5.4.7 修复:特殊字符转义)
443
+
444
+ v6.4.9: 多词查询先经 _prune_fts_query 剔除高频短语——
445
+ trigram 索引下全库短语的 bm25 评分是 100k 延迟的大头。
446
+ """
450
447
  try:
451
448
  escaped = self._escape_fts5_query(query)
449
+ if not escaped:
450
+ return []
451
+ escaped = self._prune_fts_query(conn, query, escaped)
452
452
  if not escaped:
453
453
  return []
454
454
  rows = conn.execute("""
@@ -474,3 +474,62 @@ class IndexEngine:
474
474
  return results
475
475
  except sqlite3.OperationalError:
476
476
  return []
477
+
478
+ # v6.4.9: 高频短语剪枝比例(与 TF-IDF max_df 0.05 对齐)
479
+ _FTS_MAX_DF_RATIO = 0.05
480
+
481
+ def _prune_fts_query(self, conn: sqlite3.Connection, query: str,
482
+ escaped: str) -> str:
483
+ """剔除 MATCH 里必然无区分度的高频短语。
484
+
485
+ 短语 df 上界 = min(该词全部 trigram 的 df)(短语匹配行 ⊆
486
+ 每个 trigram 行集的交集)。上界超库容 5% 的词剔除;全被剔除
487
+ 时留上界最小的那个,防空 MATCH。
488
+ df 来自 fts5vocab 的进程内缓存(首查全表读一次,此后 O(1)
489
+ 点查;以 memories.max_rowid 为失效标记,覆盖 add/update;
490
+ 硬删导致的 df 缓存偏大只会让剪枝偏保守,5% 阈值级近似)。
491
+ """
492
+ tokens = [t for t in query.split() if t]
493
+ if len(tokens) <= 1:
494
+ return escaped
495
+ try:
496
+ row = conn.execute(
497
+ "SELECT max(rowid) FROM memories").fetchone()
498
+ total = (row[0] or 0) if row else 0
499
+ if total < 200:
500
+ return escaped # 小库 df 语义不稳
501
+ if (self._fts_vocab_cache is None
502
+ or total != self._fts_vocab_rowid):
503
+ # v6.4.9: vocab 是流式虚拟表,逐 term 点查/IN 批查
504
+ # 都触发全表扫(combined 100k 45ms 回退的根因)——
505
+ # 全表读一次进 dict,之后纯内存点查。
506
+ conn.execute(
507
+ "CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts_v "
508
+ "USING fts5vocab('memory_fts', 'row')")
509
+ self._fts_vocab_cache = dict(
510
+ conn.execute("SELECT term, doc FROM memory_fts_v"))
511
+ self._fts_vocab_rowid = total
512
+ df_map = self._fts_vocab_cache
513
+ budget = self._FTS_MAX_DF_RATIO * total
514
+ kept = []
515
+ best, best_ub = None, None
516
+ for tok in tokens:
517
+ if len(tok) < 3:
518
+ ub = 0 # trigram 查不了短词,保留
519
+ else:
520
+ ub = min(df_map.get(tok[i:i + 3], 0)
521
+ for i in range(len(tok) - 2))
522
+ if ub <= budget:
523
+ kept.append(tok)
524
+ if best_ub is None or ub < best_ub:
525
+ best, best_ub = tok, ub
526
+ if not kept:
527
+ kept = [best] if best else []
528
+ return " OR ".join(
529
+ '"' + t.replace('"', '""') + '"' for t in kept)
530
+ if len(kept) == len(tokens):
531
+ return escaped
532
+ return " OR ".join(
533
+ '"' + t.replace('"', '""') + '"' for t in kept)
534
+ except sqlite3.Error:
535
+ return escaped
@@ -257,27 +257,15 @@ class QueryEngine:
257
257
  _bd(doc_id)["winner"] = "tfidf"
258
258
  _bd(doc_id)["final"] = s
259
259
 
260
- # 路 2:Fuzzy(TF-IDF 召回不足时补充;v5.7.1:加密模式下总是跑,
261
- # 供加密兜底复用结果,避免二次全表扫描)
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
- # 供加密兜底复用)。
260
+ # 路 2:Fuzzy(拼写纠错兜底;v5.7.1:加密模式下总是跑供加密
261
+ # 兜底复用,避免二次全表扫描)
262
+ # v6.4.8: 触发收紧为「TF-IDF 零有效命中」。fuzzy 的价值在
263
+ # 「查询词全不在库」(打错字场景),此时 TF-IDF 必空。有命中
264
+ # (含低分)时 fuzzy 只补凑数条目还白扫候选集(30k 实测
265
+ # 140ms/查询)。全库词回退(tfidf_fallback)同理跳过。
266
+ # 注意按非零分判定:空查询走零分 top-K 兼容分支,
267
+ # tfidf_results 恒非空,不看分会死锁拼写兜底。
277
268
  tfidf_fallback = getattr(self.index, "last_fallback", False)
278
- # v6.4.8: 「有效命中」= TF-IDF 存在非零分结果。空查询(全 miss/
279
- # 零范数)走文档序 0 分 top-K 兼容分支,tfidf_results 恒非空但
280
- # 全零分——必须按分数判定,否则 fuzzy 拼写兜底被死锁。
281
269
  tfidf_effective = any(s > 0.0 for _, s in tfidf_results)
282
270
  fuzzy_hits = []
283
271
  if (self.storage.encrypted
@@ -296,27 +284,29 @@ class QueryEngine:
296
284
  _bd(entry.id)["final"] = s
297
285
 
298
286
  # 路 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)。
287
+ # v6.4.9: 全库词回退时跳过(FTS5 对高频短语的 bm25 全匹配行
288
+ # 评分是病态查询的主要剩余成本,且无区分度信号)。正常查询
289
+ # 走 fts_search 的 df 剪枝。
303
290
  fts5_used = False
304
- # v5.6.2 安全修复:初始化默认值,防止异常路径下 UnboundLocalError
291
+ # v5.6.2: 防异常路径下 UnboundLocalError
305
292
  strategy_used = "tfidf+fuzzy"
306
293
  try:
307
- conn = self.storage._get_conn()
308
- fts_results = self.index.fts_search(conn, query, top_k=max_results * 3)
309
- for doc_id, s in fts_results:
310
- if explain:
311
- _bd(doc_id)["fts5"] = max(s, _bd(doc_id)["fts5"])
312
- if s >= score_map.get(doc_id, 0.0):
313
- score_map[doc_id] = s
294
+ if not tfidf_fallback:
295
+ conn = self.storage._get_conn()
296
+ fts_results = self.index.fts_search(conn, query, top_k=max_results * 3)
297
+ for doc_id, s in fts_results:
314
298
  if explain:
315
- _bd(doc_id)["winner"] = "fts5"
316
- _bd(doc_id)["final"] = s
317
- if fts_results:
318
- fts5_used = True
319
- strategy_used = "tfidf+fuzzy+fts5"
299
+ _bd(doc_id)["fts5"] = max(s, _bd(doc_id)["fts5"])
300
+ if s >= score_map.get(doc_id, 0.0):
301
+ score_map[doc_id] = s
302
+ if explain:
303
+ _bd(doc_id)["winner"] = "fts5"
304
+ _bd(doc_id)["final"] = s
305
+ if fts_results:
306
+ fts5_used = True
307
+ strategy_used = "tfidf+fuzzy+fts5"
308
+ else:
309
+ strategy_used = "tfidf+fuzzy"
320
310
  else:
321
311
  strategy_used = "tfidf+fuzzy"
322
312
  except Exception:
@@ -9127,19 +9127,10 @@ 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 兜底路径同享此预算(候选爆炸对加密库同样不可接受)。
9130
+ # v6.4.8: 候选集预算。短条目库 + 16 字级中文查询会让候选
9131
+ # ≈ 全库(30k 实测 1642ms);超预算只发生在无区分度查询,
9132
+ # 平分数下截断不改 top-K。排序:精确子串命中(LIKE,最强
9133
+ # fuzzy 信号)排最前,短文本兜底路按 rowid 截断。
9143
9134
  _FUZZY_CAND_CAP = 2000
9144
9135
  cand_query += (
9145
9136
  " ORDER BY (CASE WHEN content LIKE ? THEN 0 ELSE 1 END), rowid"
@@ -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.8"
23
+ __version__: str = "6.4.9"
24
24
 
25
25
  #: 结构化版本元组,便于比较(如 `VERSION_INFO >= (6, 1, 0)`)
26
- VERSION_INFO: Tuple[int, int, int] = (6, 4, 8)
26
+ VERSION_INFO: Tuple[int, int, int] = (6, 4, 9)
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.8
3
+ Version: 6.4.9
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.8: no team, no funding, 1,252 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.9: no team, no funding, 1,257 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.8-green.svg)](https://github.com/opok-ops/MindForge/releases)
43
+ [![Version](https://img.shields.io/badge/version-6.4.9-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,15 +95,15 @@ MindForge stats
95
95
  ```
96
96
 
97
97
 
98
- ## Scale & Quality Benchmarks (v6.4.8, measured)
98
+ ## Scale & Quality Benchmarks (v6.4.9, measured)
99
99
 
100
100
  | Scale | add throughput | search p50 (rare token) | search p50 (realistic mixed) | search p50 (pathological all-common) | db size |
101
101
  |---|---|---|---|---|---|
102
102
  | 200 entries (baseline) | — | 1.8 ms | — | — | <1 MB |
103
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 |
104
+ | 100,000 (~10M chars) | ~546/s | 6.8 ms | 3.0 ms | 0.4 ms | 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).
106
+ - **Ten-million-character library, millisecond-class across all query types (v6.4.9)**: pathological 218→**0.4ms** (−99.8%), mixed 143→**3.0ms** (−98%), rare 6.8ms, combined 17.8ms (100k full-pipeline, clean environment). The last bottleneck — FTS5 high-frequency-phrase bm25 scoring — is now pruned query-side via trigram-df pre-check (df upper bound = min trigram df, pruned at 5% corpus size, symmetric to the TF-IDF max_df design; df served from an in-process cache — fts5vocab is a streaming virtual table, per-term probes scan it fully).
107
107
  - **Index-blindness fix (v6.4.8)**: TF-IDF vocabulary was locked to the first 5 documents since v6.4.6 — incremental adds indexed as empty vectors (TF-IDF effectively half-dead; previous numbers carried by FTS5/fuzzy). Fixed with incremental vocabulary growth; all pipeline numbers above are now *true full-pipeline* measurements.
108
108
  - Retrieval quality (fixed-seed, `benchmarks/quality_bench.py`, 2k entries): needle Recall@1 = 1.0, MRR = 1.0, topic Recall@1 = 1.0, noise false-recall = 0.
109
109
  - Full LLM-judged benchmarks (LoCoMo / LongMemEval) — interface ready, not yet run (requires external LLM API key).
@@ -111,7 +111,9 @@ MindForge stats
111
111
 
112
112
  ## What's New
113
113
 
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).
114
+ v6.1.3 – v6.4.9 shipped in ten days (2026-11-01 → 11-11; four releases in the last two days), guarded by **1,258 tests** (1,257 passing, 1 environment-skipped).
115
+
116
+ - **v6.4.9 — ten-million-char library at millisecond latency: FTS5 high-frequency-phrase scoring pruned** (2026-11-11). The last honest bottleneck from v6.4.8's ledger. Root cause (30k per-path timing): under the trigram tokenizer, corpus-wide phrases (template data's "陈述性记忆") match every row — bm25 full-match scoring cost 50.5ms/query, the dominant remainder of 100k mixed latency. Fix, query-side df pre-pruning symmetric to TF-IDF max_df: a phrase's df upper bound = min df of its trigrams (phrase matches ⊆ every trigram's row set); prune at 5% of corpus size; keep the lowest-ub phrase if all pruned. df is served from an in-process cache keyed by `max(rowid)` (add/update invalidate; hard-delete leaves df slightly stale = prunes more conservatively) — **two real pitfalls found en route: fts5vocab is a *streaming virtual table* — per-term probes AND `IN` batches trigger full scans (combined regressed 17→45ms before the cache)**. Plus: FTS5 path skipped on pathological fallback (no discriminating signal). Clean-environment 100k re-bench: pathological 122→**0.4ms** (−99.7%), mixed 134.9→**3.0ms** (−98%), all query types millisecond-class at ~10M chars. AI-flavored comment cleanup (essay-style docstrings → engineer-speak, info preserved). 5 new guards.
115
117
 
116
118
  - **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.
117
119
 
@@ -186,7 +188,7 @@ v6.1.3 – v6.4.8 shipped in ten days (2026-11-01 → 11-11; three releases in t
186
188
 
187
189
  ```
188
190
  ┌──────────────────────────────────────────────────────┐
189
- │ MindForge v6.4.8 │
191
+ │ MindForge v6.4.9 │
190
192
  │ Cognitive Layer Personality · KnowledgeGraph │
191
193
  │ MemoryEvolution · FederatedMemory │
192
194
  ├──────────────────────────────────────────────────────┤
@@ -136,4 +136,5 @@ tests/test_v643_fixes.py
136
136
  tests/test_v645_fixes.py
137
137
  tests/test_v646_fixes.py
138
138
  tests/test_v648_fixes.py
139
+ tests/test_v649_fixes.py
139
140
  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.8"
42
+ _EXPECTED_VERSION = "6.4.9"
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.8")
44
- self.assertEqual(core_version.VERSION_INFO, (6, 4, 8))
43
+ self.assertEqual(core_version.__version__, "6.4.9")
44
+ self.assertEqual(core_version.VERSION_INFO, (6, 4, 9))
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.8")
666
- self.assertEqual(VERSION_INFO, (6, 4, 8))
665
+ self.assertEqual(__version__, "6.4.9")
666
+ self.assertEqual(VERSION_INFO, (6, 4, 9))
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.8")
97
+ self.assertEqual(__version__, "6.4.9")
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.8")
104
+ self.assertEqual(__version__, "6.4.9")
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.8")
88
+ self.assertEqual(__version__, "6.4.9")
89
89
 
90
90
 
91
91
  if __name__ == "__main__":
@@ -22,7 +22,7 @@ import unittest
22
22
 
23
23
  sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
24
24
 
25
- from mindforge.core.version import __version__
25
+ from mindforge.core.version import __version__, VERSION_INFO
26
26
  from mindforge.core.indexer import VectorIndex
27
27
 
28
28
 
@@ -181,8 +181,10 @@ class TestFuzzyGateAndFallbackSignal(unittest.TestCase):
181
181
  class TestVersionBump(unittest.TestCase):
182
182
  """版本钉值(v6.4.5 教训:每版 fixes 断言随发版一并 bump)。"""
183
183
 
184
- def test_version_is_648(self):
185
- self.assertEqual(__version__, "6.4.8")
184
+ def test_version_is_648_plus(self):
185
+ # v6.4.9 发版时 bump:本文件守卫 v6.4.8 引入的行为,
186
+ # 版本断言随最新发版跟进
187
+ self.assertGreaterEqual(VERSION_INFO, (6, 4, 8))
186
188
 
187
189
 
188
190
  if __name__ == "__main__":