mindforge-memory 6.4.5__tar.gz → 6.4.8__tar.gz

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