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.
- {mindforge_memory-6.4.8/mindforge_memory.egg-info → mindforge_memory-6.4.9}/PKG-INFO +10 -8
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/README.md +9 -7
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/indexer.py +81 -22
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/query.py +27 -37
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/storage.py +4 -13
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/version.py +2 -2
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9/mindforge_memory.egg-info}/PKG-INFO +10 -8
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/SOURCES.txt +1 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v568_fixes.py +1 -1
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v575_release_consistency.py +2 -2
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v612_fixes.py +2 -2
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v643_fixes.py +1 -1
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v645_fixes.py +1 -1
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v646_fixes.py +1 -1
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v648_fixes.py +5 -3
- mindforge_memory-6.4.9/tests/test_v649_fixes.py +126 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/LICENSE +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/MindForge.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/claude_adapter.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/crewai_memory.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/generic_api.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/langgraph_store.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/adapters/openclaw_adapter.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/api/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/api/server.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/cli/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/cli/main.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/embedding.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/encryption.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/mindforge.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/paths.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/core/types.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/daemon/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/daemon/server.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/mcp/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/mcp/server.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/__init__.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/categorizer.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/conflict_detector.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/connectors.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/context_window.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/event_bus.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/evolution.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/experience.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/federated.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/federated_acl.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/follow_up.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/hybrid_search.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/importer.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/integrator.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/intent_router.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/knowledge_graph.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/memory_compressor.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/memory_decay.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/memory_passport.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/multi_agent.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/multimodal.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/outcome_learning.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/personality.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/principled_forgetting.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/privacy.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/recall.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/session_focus.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/share_conflict.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/skill_extractor.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/skill_pack.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/timeline_viz.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge/modules/v6.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/dependency_links.txt +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/entry_points.txt +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/requires.txt +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/mindforge_memory.egg-info/top_level.txt +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/pyproject.toml +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/setup.cfg +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/setup.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_core.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_security_hardening.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_trash_hard_delete.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v546_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v547_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v548_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v550_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v552_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v554_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v556_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v557_banner_insight_wal.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v557_security_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v557_webhook_sig_export.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v558_audit_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v558_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v560_decay_gc.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v560_kdf_versioning.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v560_module_coverage.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v561_security.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v562_p2_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v563_entry_coverage.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v563_perf_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v565_security.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v566_encrypted_search.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v567_security_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v569_fts_sync.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v570_coverage_expansion.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v570_drama_and_utils.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v570_encrypted_fallback.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v571_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v572_security.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v573_encrypted_export.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v573_security.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v576_bitemporal.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v576_gdpr.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v577_agent_governance.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v578_embedding.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v579_graph.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v580_experience.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v5812_bugfixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v583_security.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v584_adapters.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v585_benchmark.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v586_federated_mcp.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v590_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v592_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v593_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v595_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_forgetting_followup.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_import_timeline.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_multi_agent_formal.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_outcome_passport.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v600_skillpack_evolution.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v611_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v612_cascade_fixes.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v613_integrity.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v620_growth_governance.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v630_search_explain.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v631_scrypt.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v632_explain_interfaces.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v635_features.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v635_integration.py +0 -0
- {mindforge_memory-6.4.8 → mindforge_memory-6.4.9}/tests/test_v636_fixes.py +0 -0
- {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.
|
|
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.
|
|
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
|
[](https://www.python.org/downloads/)
|
|
42
42
|
[](LICENSE)
|
|
43
|
-
[](https://github.com/opok-ops/MindForge/releases)
|
|
44
44
|
[](https://github.com/opok-ops/MindForge/stargazers)
|
|
45
45
|
[](https://github.com/opok-ops/MindForge/forks)
|
|
46
46
|
[](https://github.com/opok-ops/MindForge/issues)
|
|
@@ -95,15 +95,15 @@ MindForge stats
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
## Scale & Quality Benchmarks (v6.4.
|
|
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 | ~
|
|
104
|
+
| 100,000 (~10M chars) | ~546/s | 6.8 ms | 3.0 ms | 0.4 ms | 98 MB |
|
|
105
105
|
|
|
106
|
-
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
[](https://www.python.org/downloads/)
|
|
12
12
|
[](LICENSE)
|
|
13
|
-
[](https://github.com/opok-ops/MindForge/releases)
|
|
14
14
|
[](https://github.com/opok-ops/MindForge/stargazers)
|
|
15
15
|
[](https://github.com/opok-ops/MindForge/forks)
|
|
16
16
|
[](https://github.com/opok-ops/MindForge/issues)
|
|
@@ -65,15 +65,15 @@ MindForge stats
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
## Scale & Quality Benchmarks (v6.4.
|
|
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 | ~
|
|
74
|
+
| 100,000 (~10M chars) | ~546/s | 6.8 ms | 3.0 ms | 0.4 ms | 98 MB |
|
|
75
75
|
|
|
76
|
-
-
|
|
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.
|
|
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.
|
|
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:
|
|
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
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
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
|
|
352
|
-
#
|
|
353
|
-
#
|
|
354
|
-
# transform
|
|
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
|
|
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
|
|
261
|
-
#
|
|
262
|
-
# v6.4.8
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
#
|
|
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.
|
|
300
|
-
#
|
|
301
|
-
#
|
|
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
|
|
291
|
+
# v5.6.2: 防异常路径下 UnboundLocalError
|
|
305
292
|
strategy_used = "tfidf+fuzzy"
|
|
306
293
|
try:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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)["
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
|
9131
|
-
#
|
|
9132
|
-
#
|
|
9133
|
-
#
|
|
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.
|
|
23
|
+
__version__: str = "6.4.9"
|
|
24
24
|
|
|
25
25
|
#: 结构化版本元组,便于比较(如 `VERSION_INFO >= (6, 1, 0)`)
|
|
26
|
-
VERSION_INFO: Tuple[int, int, int] = (6, 4,
|
|
26
|
+
VERSION_INFO: Tuple[int, int, int] = (6, 4, 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.
|
|
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.
|
|
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
|
[](https://www.python.org/downloads/)
|
|
42
42
|
[](LICENSE)
|
|
43
|
-
[](https://github.com/opok-ops/MindForge/releases)
|
|
44
44
|
[](https://github.com/opok-ops/MindForge/stargazers)
|
|
45
45
|
[](https://github.com/opok-ops/MindForge/forks)
|
|
46
46
|
[](https://github.com/opok-ops/MindForge/issues)
|
|
@@ -95,15 +95,15 @@ MindForge stats
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
## Scale & Quality Benchmarks (v6.4.
|
|
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 | ~
|
|
104
|
+
| 100,000 (~10M chars) | ~546/s | 6.8 ms | 3.0 ms | 0.4 ms | 98 MB |
|
|
105
105
|
|
|
106
|
-
-
|
|
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.
|
|
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.
|
|
191
|
+
│ MindForge v6.4.9 │
|
|
190
192
|
│ Cognitive Layer Personality · KnowledgeGraph │
|
|
191
193
|
│ MemoryEvolution · FederatedMemory │
|
|
192
194
|
├──────────────────────────────────────────────────────┤
|
|
@@ -40,8 +40,8 @@ class TestVersionTruth(unittest.TestCase):
|
|
|
40
40
|
def test_core_version_matches_release(self):
|
|
41
41
|
# 版本推进时必须一并更新此处的钉值(与 test_v568_fixes 的
|
|
42
42
|
# _EXPECTED_VERSION 同一约定:让「忘记 bump 版本」在测试里显形)。
|
|
43
|
-
self.assertEqual(core_version.__version__, "6.4.
|
|
44
|
-
self.assertEqual(core_version.VERSION_INFO, (6, 4,
|
|
43
|
+
self.assertEqual(core_version.__version__, "6.4.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.
|
|
666
|
-
self.assertEqual(VERSION_INFO, (6, 4,
|
|
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 兜底)。"""
|
|
@@ -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
|
|
185
|
-
|
|
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__":
|