memleaf 0.2.19__tar.gz → 0.2.21__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 (111) hide show
  1. {memleaf-0.2.19 → memleaf-0.2.21}/CHANGELOG.md +14 -0
  2. {memleaf-0.2.19/src/memleaf.egg-info → memleaf-0.2.21}/PKG-INFO +8 -6
  3. {memleaf-0.2.19 → memleaf-0.2.21}/README.en.md +11 -7
  4. {memleaf-0.2.19 → memleaf-0.2.21}/README.md +7 -5
  5. {memleaf-0.2.19 → memleaf-0.2.21}/pyproject.toml +1 -1
  6. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/adapters/hermes.py +3 -2
  8. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/compaction.py +8 -0
  9. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/hermes_provider/__init__.py +12 -6
  10. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  11. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/host_events.py +55 -6
  12. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/host_runtime.py +43 -3
  13. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/installer.py +7 -4
  14. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/mcp_server.py +91 -6
  15. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/memory_writer.py +12 -2
  16. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/models.py +16 -1
  17. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/processing.py +38 -1
  18. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/prompts.py +19 -11
  19. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/retrieval_gate.py +106 -20
  20. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/service.py +162 -0
  21. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/validation.py +86 -2
  22. {memleaf-0.2.19 → memleaf-0.2.21/src/memleaf.egg-info}/PKG-INFO +8 -6
  23. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf.egg-info/SOURCES.txt +2 -0
  24. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_context_budget.py +2 -2
  25. memleaf-0.2.21/tests/test_global_todo_acceptance.py +288 -0
  26. memleaf-0.2.21/tests/test_global_todo_retrieval.py +112 -0
  27. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_pypi_install.py +9 -2
  28. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_retrieval_gate.py +8 -9
  29. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_c1_mcp.py +5 -3
  30. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_c2_init.py +1 -1
  31. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_c3_packaging.py +1 -1
  32. memleaf-0.2.21/tests/test_v2_gate_limits.py +111 -0
  33. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_v2_mcp_flow.py +9 -10
  34. memleaf-0.2.19/tests/test_v2_gate_limits.py +0 -207
  35. {memleaf-0.2.19 → memleaf-0.2.21}/IMPLEMENTATION_PLAN.md +0 -0
  36. {memleaf-0.2.19 → memleaf-0.2.21}/LICENSE +0 -0
  37. {memleaf-0.2.19 → memleaf-0.2.21}/MANIFEST.in +0 -0
  38. {memleaf-0.2.19 → memleaf-0.2.21}/RELEASE_CHECKLIST.md +0 -0
  39. {memleaf-0.2.19 → memleaf-0.2.21}/examples/README.md +0 -0
  40. {memleaf-0.2.19 → memleaf-0.2.21}/examples/basic_usage.py +0 -0
  41. {memleaf-0.2.19 → memleaf-0.2.21}/examples/mcp_stdio.ndjson +0 -0
  42. {memleaf-0.2.19 → memleaf-0.2.21}/install.ps1 +0 -0
  43. {memleaf-0.2.19 → memleaf-0.2.21}/install.sh +0 -0
  44. {memleaf-0.2.19 → memleaf-0.2.21}/setup.cfg +0 -0
  45. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/__main__.py +0 -0
  46. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/adapters/__init__.py +0 -0
  47. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/adapters/antigravity.py +0 -0
  48. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/adapters/base.py +0 -0
  49. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/adapters/codex.py +0 -0
  50. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/budget.py +0 -0
  51. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/capture.py +0 -0
  52. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/cli.py +0 -0
  53. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/config.py +0 -0
  54. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/credentials.py +0 -0
  55. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/frontmatter.py +0 -0
  56. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/hermes_provider/README.md +0 -0
  57. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/inbox.py +0 -0
  58. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/index.py +0 -0
  59. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/llm/__init__.py +0 -0
  60. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/llm/base.py +0 -0
  61. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/llm/claude_compatible.py +0 -0
  62. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/llm/gemini.py +0 -0
  63. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/llm/openai_compatible.py +0 -0
  64. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/llm/router.py +0 -0
  65. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/locking.py +0 -0
  66. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/model_discovery.py +0 -0
  67. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/native_index.py +0 -0
  68. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/native_registration.py +0 -0
  69. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/redaction.py +0 -0
  70. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/retrieval.py +0 -0
  71. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/scope_maintenance.py +0 -0
  72. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/scope_state.py +0 -0
  73. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf/vault.py +0 -0
  74. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf.egg-info/dependency_links.txt +0 -0
  75. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf.egg-info/entry_points.txt +0 -0
  76. {memleaf-0.2.19 → memleaf-0.2.21}/src/memleaf.egg-info/top_level.txt +0 -0
  77. {memleaf-0.2.19 → memleaf-0.2.21}/tests/__init__.py +0 -0
  78. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_admission_noise.py +0 -0
  79. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_codex_install.py +0 -0
  80. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_codex_native_cli.py +0 -0
  81. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_credential_safety.py +0 -0
  82. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_cross_host_acceptance.py +0 -0
  83. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_cross_turn_dedupe_regressions.py +0 -0
  84. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_extraction_quality_regressions.py +0 -0
  85. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_hermes_native_registration.py +0 -0
  86. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_hermes_provider.py +0 -0
  87. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_hermes_stdio_transport.py +0 -0
  88. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_host_events.py +0 -0
  89. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_host_runtime_contract.py +0 -0
  90. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_install.py +0 -0
  91. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_maintenance_v2.py +0 -0
  92. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_model_discovery.py +0 -0
  93. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_retrieval_v2.py +0 -0
  94. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_session_lineage.py +0 -0
  95. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_a.py +0 -0
  96. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b1.py +0 -0
  97. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b2a.py +0 -0
  98. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b2b.py +0 -0
  99. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3a_commit.py +0 -0
  100. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3a_contract.py +0 -0
  101. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3b_native_context.py +0 -0
  102. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3b_native_index.py +0 -0
  103. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3b_scope.py +0 -0
  104. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3c_retrieval.py +0 -0
  105. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  106. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_todo_state_recovery.py +0 -0
  107. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_update_target_recovery.py +0 -0
  108. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_upgrade_preserves_vault.py +0 -0
  109. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_v2_host_flow.py +0 -0
  110. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_v2_nomatch_semantics.py +0 -0
  111. {memleaf-0.2.19 → memleaf-0.2.21}/tests/test_v2_search_gate_acceptance.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.21 — 2026-09-03
6
+
7
+ - Fix `memleaf install` for Hermes after the 12th MCP tool was added: the installer now uses the adapter's canonical tool-count constant instead of a stale private `11`.
8
+ - Add a regression invariant that compares the Hermes installer count with the actual public MCP tool tuple, preventing future tool additions from silently breaking installation self-checks.
9
+ - Correct the English README's stale 11-tool and 3-memory/6000-character descriptions so the published documentation matches the v0.2 retrieval contract.
10
+
11
+ ## 0.2.20 — 2026-09-03
12
+
13
+ - Add `list_todos` for complete active todo retrieval across all scopes with stable pagination, date/status filtering, and current-turn retrieval gating.
14
+ - Remove the managed-turn 3-memory / 6000-character aggregate read block while retaining the 2000-character page limit and audit counters.
15
+ - Add first-class `todo.due_date` (`YYYY-MM-DD`) with evidence-grounded date normalization, update/history/compaction propagation, and legacy Markdown compatibility.
16
+ - Improve automatic extraction so concrete unfinished actions become atomic todos and durable rules remain project/fact memories; mixed rule/action candidates are split or safely deferred without blocking valid siblings.
17
+ - Keep permanent `knowledge/` globally visible to all sessions and supported Agents sharing the same Vault; provenance fields never filter active memory visibility.
18
+
5
19
  ## 0.2.19 — 2026-09-03
6
20
 
7
21
  - Keep Hermes `<untrusted_tool_result>` search audit status synchronized with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.19
3
+ Version: 0.2.21
4
4
  Summary: A local-first Markdown memory core for AI agents
5
5
  Author: memleaf contributors
6
6
  License-Expression: MIT
@@ -23,8 +23,8 @@ Dynamic: license-file
23
23
 
24
24
  [English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
25
25
 
26
- > **当前版本:0.2.19。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.19 通过 PyPI 分发。
26
+ > **当前版本:0.2.21。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.21 通过 PyPI 分发。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
30
30
  ## 项目定位
@@ -69,12 +69,14 @@ memleaf 不会把整个 Vault 或整段历史对话自动塞进模型上下文
69
69
  4. 只有需要引用事实时,才使用同一轮返回的 `retrieval_id` 调用 `read`;
70
70
  5. 根据读取到的关键记忆回答,不把所有候选全部读完。
71
71
 
72
+ 当用户询问当前待办、所有未完成工作、紧急事项或某个日期范围内必须完成的事项时,使用 `list_todos`,默认覆盖所有 Scope;持续分页直到 `has_more=false`,再用同一个 `retrieval_id` 读取所有匹配待办正文。`source/session_id/turn_id` 只用于来源追踪,不参与永久 knowledge 可见性。
73
+
72
74
  当前限制:
73
75
 
74
76
  - Scope Map 最多 20 项、约 2000 字符;
75
77
  - search 候选单页最多 20 项、约 4000 字符;
76
78
  - read 单页正文最多 2000 字符;
77
- - 受管理轮次最多读取 3 个不同记忆、累计 6000 个正文字符;
79
+ - 同一受管理轮次可继续读取所有与问题相关的记忆;`read_count`/`read_chars` 仅用于审计,不再作为阻断配额;
78
80
  - `retrieval_id` 必须属于当前轮次,且必须先有成功的 `search` 才能 `read`;
79
81
  - `found`、`no_match` 和工具错误是不同状态,错误不能被伪装成无匹配;
80
82
  - `context()` 和 Python `search(view="full")` 作为兼容接口保留,但不属于自动检索路径。
@@ -157,12 +159,12 @@ python -m pip install -U memleaf && python -m memleaf install
157
159
  4. 激活 `memory.provider=memleaf`;
158
160
  5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
159
161
  6. 配置 MCP lazy/idle 生命周期;
160
- 7. 验证 MCP Server 能发现 11 个工具;
162
+ 7. 验证 MCP Server 能发现 12 个工具;
161
163
  8. 写入本地 Agent 状态索引。
162
164
 
163
165
  完成后重启 Hermes。
164
166
 
165
- 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 11 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
167
+ 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 12 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
166
168
 
167
169
  仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
168
170
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  [中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
6
6
 
7
- > **Version: 0.2.19.**
8
- > The core library, Vault, stdio MCP server, initialization CLI, model routing, memory extraction, controlled retrieval protocol, and host adapters are implemented. memleaf 0.2.19 is distributed through PyPI.
7
+ > **Version: 0.2.21.**
8
+ > The core library, Vault, stdio MCP server, initialization CLI, model routing, memory extraction, controlled retrieval protocol, and host adapters are implemented. memleaf 0.2.21 is distributed through PyPI.
9
9
  > **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
10
10
 
11
11
  ## Project scope
@@ -55,7 +55,7 @@ Current limits:
55
55
  - Scope Map: at most 20 items and approximately 2,000 characters per page;
56
56
  - search candidates: at most 20 items and approximately 4,000 characters per page;
57
57
  - one read page: at most 2,000 body characters;
58
- - managed turn: at most 3 distinct memory IDs and 6,000 body characters in total;
58
+ - managed turn: all relevant memories may be read; `read_count` and `read_chars` are audit-only and do not block valid reads;
59
59
  - `retrieval_id` must belong to the current turn, and a successful `search` is required before `read`;
60
60
  - `found`, `no_match`, and tool errors are distinct states; an error must not be reported as no match;
61
61
  - `context()` and Python `search(view="full")` remain available for compatibility, but are not part of the automatic retrieval path.
@@ -80,6 +80,9 @@ Additional rules:
80
80
  - Model, parsing, write, or index failures keep the inbox and processing watermark retryable.
81
81
  - Automatic cleanup has a 24-hour safety period; a failed processing attempt does not delete the original capture.
82
82
 
83
+
84
+ For current todos, all unfinished work, urgent items, or work due in a date range, use `list_todos`. It covers all scopes by default; continue until `has_more=false`, then read every matching todo body with the same `retrieval_id`. Provenance fields such as `source`, `session_id`, and `turn_id` never filter permanent knowledge visibility.
85
+
83
86
  ## Installation
84
87
 
85
88
  Python 3.11+ is required. The current release supports Hermes and Codex. Hermes is the default install target; Codex uses an explicit, separate install command.
@@ -138,12 +141,12 @@ Both installation paths automatically:
138
141
  4. Activate `memory.provider=memleaf`.
139
142
  5. Configure the memleaf MCP entry through Hermes' official CLI.
140
143
  6. Configure MCP lazy/idle lifecycle settings.
141
- 7. Verify that the MCP server exposes all 11 tools.
144
+ 7. Verify that the MCP server exposes all 12 tools.
142
145
  8. Record the local Agent integration status.
143
146
 
144
147
  Restart Hermes after installation.
145
148
 
146
- If Hermes cannot be detected, no complete model route can be configured, Provider activation fails, or the 11-tool MCP verification fails, the installer returns an explicit failure rather than reporting an incomplete integration as successful.
149
+ If Hermes cannot be detected, no complete model route can be configured, Provider activation fails, or the 12-tool MCP verification fails, the installer returns an explicit failure rather than reporting an incomplete integration as successful.
147
150
 
148
151
  The repository `install.sh` remains for source development, offline source installation, and troubleshooting. Normal PyPI users do not need to run it.
149
152
 
@@ -240,7 +243,7 @@ python -m memleaf.mcp_server --vault "$HOME/.memleaf"
240
243
 
241
244
  Without `--vault`, the server uses `~/.memleaf`; `MEMLEAF_VAULT` can also specify the Vault. In normal use the server does not need to be kept running manually: Hermes or Codex starts it on demand. stdout contains only JSON-RPC messages so logs do not corrupt the protocol stream.
242
245
 
243
- The server currently exposes 11 tools:
246
+ The server currently exposes 12 tools:
244
247
 
245
248
  | Tool | Purpose |
246
249
  | --- | --- |
@@ -248,6 +251,7 @@ The server currently exposes 11 tools:
248
251
  | `context` | Compatibility directory; not used by the automatic retrieval path |
249
252
  | `scope_catalog` | Return Scopes, parents, and aliases without memory bodies |
250
253
  | `search` | Return a bounded candidate directory and `found`/`no_match` status |
254
+ | `list_todos` | Enumerate current todo memories across scopes with status/date filters and pagination |
251
255
  | `read` | Read a selected memory body in pages using the current `retrieval_id` |
252
256
  | `process` | Process complete inbox turns under the admission rules |
253
257
  | `remember` | Create or update memory after an explicit request |
@@ -256,7 +260,7 @@ The server currently exposes 11 tools:
256
260
  | `rebuild_index` | Rebuild local derived indexes |
257
261
  | `stats` | Return Vault counts and diagnostic statistics |
258
262
 
259
- Search results are clues; a title alone is not a business fact. Managed retrieval must use the same `retrieval_id` for `search → read`. Tool errors, Scope conflicts, and exhausted read budgets must be handled as such.
263
+ Search results are clues; a title alone is not a business fact. Managed retrieval must use the same `retrieval_id` for `search → read`. Tool errors, Scope conflicts, retrieval-turn violations, and read page/version errors must be handled as such.
260
264
 
261
265
  ## Python API
262
266
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  [English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
6
6
 
7
- > **当前版本:0.2.19。**
8
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.19 通过 PyPI 分发。
7
+ > **当前版本:0.2.21。**
8
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.21 通过 PyPI 分发。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
11
11
  ## 项目定位
@@ -50,12 +50,14 @@ memleaf 不会把整个 Vault 或整段历史对话自动塞进模型上下文
50
50
  4. 只有需要引用事实时,才使用同一轮返回的 `retrieval_id` 调用 `read`;
51
51
  5. 根据读取到的关键记忆回答,不把所有候选全部读完。
52
52
 
53
+ 当用户询问当前待办、所有未完成工作、紧急事项或某个日期范围内必须完成的事项时,使用 `list_todos`,默认覆盖所有 Scope;持续分页直到 `has_more=false`,再用同一个 `retrieval_id` 读取所有匹配待办正文。`source/session_id/turn_id` 只用于来源追踪,不参与永久 knowledge 可见性。
54
+
53
55
  当前限制:
54
56
 
55
57
  - Scope Map 最多 20 项、约 2000 字符;
56
58
  - search 候选单页最多 20 项、约 4000 字符;
57
59
  - read 单页正文最多 2000 字符;
58
- - 受管理轮次最多读取 3 个不同记忆、累计 6000 个正文字符;
60
+ - 同一受管理轮次可继续读取所有与问题相关的记忆;`read_count`/`read_chars` 仅用于审计,不再作为阻断配额;
59
61
  - `retrieval_id` 必须属于当前轮次,且必须先有成功的 `search` 才能 `read`;
60
62
  - `found`、`no_match` 和工具错误是不同状态,错误不能被伪装成无匹配;
61
63
  - `context()` 和 Python `search(view="full")` 作为兼容接口保留,但不属于自动检索路径。
@@ -138,12 +140,12 @@ python -m pip install -U memleaf && python -m memleaf install
138
140
  4. 激活 `memory.provider=memleaf`;
139
141
  5. 通过 Hermes 官方 CLI 配置 memleaf MCP;
140
142
  6. 配置 MCP lazy/idle 生命周期;
141
- 7. 验证 MCP Server 能发现 11 个工具;
143
+ 7. 验证 MCP Server 能发现 12 个工具;
142
144
  8. 写入本地 Agent 状态索引。
143
145
 
144
146
  完成后重启 Hermes。
145
147
 
146
- 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 11 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
148
+ 如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 12 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
147
149
 
148
150
  仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
149
151
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.19"
7
+ version = "0.2.21"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.19"
3
+ __version__ = "0.2.21"
4
4
 
5
5
  from .config import DEFAULT_CONFIG, default_config, load_config, save_config
6
6
  from .frontmatter import FrontmatterError, dump_frontmatter, dump_yaml, load_yaml, parse_frontmatter
@@ -24,7 +24,8 @@ from .base import (
24
24
 
25
25
 
26
26
  _MCP_IDLE_TIMEOUT_SECONDS = 60
27
- _MCP_EXPECTED_TOOL_COUNT = 11
27
+ MCP_EXPECTED_TOOL_COUNT = 12
28
+ _MCP_EXPECTED_TOOL_COUNT = MCP_EXPECTED_TOOL_COUNT
28
29
 
29
30
 
30
31
  def hermes_home_for_platform(
@@ -417,7 +418,7 @@ class HermesAdapter:
417
418
  self,
418
419
  detection: Detection | None = None,
419
420
  *,
420
- expected_tools: int = _MCP_EXPECTED_TOOL_COUNT,
421
+ expected_tools: int = MCP_EXPECTED_TOOL_COUNT,
421
422
  ) -> bool:
422
423
  """Run the official MCP test and require the expected tool count."""
423
424
 
@@ -40,6 +40,7 @@ def _content_mapping(value: Memory | Mapping[str, Any]) -> dict[str, Any]:
40
40
  "keywords": list(value.keywords),
41
41
  "status": value.status,
42
42
  "completed_at": value.completed_at,
43
+ "due_date": value.due_date,
43
44
  }
44
45
  if not isinstance(value, Mapping):
45
46
  raise ValueError("memory content must be a memory or mapping")
@@ -54,6 +55,7 @@ def _content_mapping(value: Memory | Mapping[str, Any]) -> dict[str, Any]:
54
55
  "keywords": list(value.get("keywords", [])),
55
56
  "status": value.get("status"),
56
57
  "completed_at": value.get("completed_at"),
58
+ "due_date": value.get("due_date"),
57
59
  }
58
60
 
59
61
 
@@ -423,6 +425,7 @@ class Compactor:
423
425
  "keywords",
424
426
  "status",
425
427
  "completed_at",
428
+ "due_date",
426
429
  )
427
430
  },
428
431
  }
@@ -511,6 +514,7 @@ class Compactor:
511
514
  last_hit_at=self._last_hit(source_memories),
512
515
  status=status,
513
516
  completed_at=summary.get("completed_at"),
517
+ due_date=summary.get("due_date"),
514
518
  extra=extra,
515
519
  )
516
520
  raw = memory.to_markdown()
@@ -540,6 +544,7 @@ class Compactor:
540
544
  and left.sources == right.sources
541
545
  and left.status == right.status
542
546
  and left.completed_at == right.completed_at
547
+ and left.due_date == right.due_date
543
548
  and left_extra == right_extra
544
549
  )
545
550
 
@@ -591,6 +596,8 @@ class Compactor:
591
596
  replacements: list[_Replacement] = []
592
597
  for summary in output["memories"]:
593
598
  source_ids = tuple(summary["source_memory_ids"])
599
+ if summary.get("type") == "todo" and len(source_ids) != 1:
600
+ raise CompactionError("independent todos cannot be merged by compaction")
594
601
  try:
595
602
  source_candidates = [by_id[source_id.casefold()] for source_id in source_ids]
596
603
  except KeyError as error:
@@ -638,6 +645,7 @@ class Compactor:
638
645
  last_hit_at=source.memory.last_hit_at,
639
646
  status=source.memory.status,
640
647
  completed_at=source.memory.completed_at,
648
+ due_date=source.memory.due_date,
641
649
  extra=extra,
642
650
  )
643
651
  return history_id, historical, historical.to_markdown()
@@ -268,8 +268,9 @@ def _scope_context(
268
268
  "<memleaf-scope-map>\n"
269
269
  "Available memleaf memory scopes. For every visible user turn, call "
270
270
  "memleaf MCP search at least once using the current conversation and "
271
- "this map. Search returns a directory; read only the selected memory "
272
- "when needed. A no-match result is valid.\n"
271
+ "this map. Use list_todos instead of relevance search for global current-todo questions. "
272
+ "Search/list_todos return directories; read only the selected memory when needed for ordinary "
273
+ "relevance queries; for global todo queries, read every matching todo item. A no-match result is valid.\n"
273
274
  )
274
275
  if isinstance(retrieval_id, str) and retrieval_id:
275
276
  prefix = (
@@ -421,11 +422,12 @@ def _hermes_search_status(value: Any) -> str:
421
422
  return "error"
422
423
  valid_results = all(
423
424
  isinstance(item, Mapping)
424
- and set(item) == {"memory_id", "title"}
425
+ and set(item) in ({"memory_id", "title"}, {"memory_id", "title", "due_date"})
425
426
  and isinstance(item.get("memory_id"), str)
426
427
  and bool(item.get("memory_id"))
427
428
  and isinstance(item.get("title"), str)
428
429
  and bool(item.get("title"))
430
+ and ("due_date" not in item or item.get("due_date") is None or isinstance(item.get("due_date"), str))
429
431
  for item in results
430
432
  )
431
433
  if not valid_results:
@@ -1953,8 +1955,12 @@ class MemleafMemoryProvider(MemoryProvider):
1953
1955
  "read(memory_id, retrieval_id) when its body is needed, carrying the "
1954
1956
  "current turn's retrieval_id exactly as supplied; a missing or mismatched "
1955
1957
  "token is a read failure, never a reason to fall back to a file tool. "
1956
- "Read more only if needed, and do not read all entries to filter unrelated "
1957
- "items. Hermes has a soft "
1958
+ "Read more only if needed; for ordinary relevance queries, do not read all entries to filter unrelated items. "
1959
+ "When the user asks for current "
1960
+ "todos, all unfinished work, urgent work, or work due in a time range, call memleaf MCP "
1961
+ "list_todos instead of relevance search; omit scope for a global query, follow every "
1962
+ "next_cursor until has_more=false, and read every matching todo body with the same retrieval_id. "
1963
+ "Never exclude a todo because another Hermes session or another Agent created it. Hermes has a soft "
1958
1964
  "observer only: do not claim a search happened unless the visible tool "
1959
1965
  "messages show it. Visible Hermes "
1960
1966
  "turns are durably captured into the local memleaf inbox. Automatic "
@@ -2155,7 +2161,7 @@ class MemleafMemoryProvider(MemoryProvider):
2155
2161
  search_results_used: set[int] = set()
2156
2162
  search_ordinal = 0
2157
2163
  for call in calls:
2158
- if call.get("name") != "mcp__memleaf__search":
2164
+ if call.get("name") not in {"mcp__memleaf__search", "mcp__memleaf__list_todos"}:
2159
2165
  continue
2160
2166
  search_ordinal += 1
2161
2167
  arguments = call.get("arguments")
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.19
2
+ version: 0.2.21
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -32,6 +32,7 @@ from .retrieval_gate import (
32
32
  find_pending_continuation,
33
33
  mark_degraded,
34
34
  observe_search,
35
+ todo_filter_key,
35
36
  request_gate_retry,
36
37
  validate_turn,
37
38
  )
@@ -306,7 +307,11 @@ def _handle_codex_pre_tool(
306
307
  turn_id: str | None,
307
308
  ) -> dict[str, Any]:
308
309
  tool_name = _field(event, "tool_name", "toolName")
309
- if not (_codex_memleaf_tool(tool_name, "search") or _codex_memleaf_tool(tool_name, "read")):
310
+ if not (
311
+ _codex_memleaf_tool(tool_name, "search")
312
+ or _codex_memleaf_tool(tool_name, "list_todos")
313
+ or _codex_memleaf_tool(tool_name, "read")
314
+ ):
310
315
  return {}
311
316
  if session_id is None or turn_id is None:
312
317
  return {}
@@ -385,26 +390,70 @@ def _codex_search_status(value: Any) -> str:
385
390
  return "error"
386
391
 
387
392
 
393
+
394
+ def _codex_todo_result(value: Any) -> tuple[str, bool, str | None]:
395
+ result = _json_tool_result(value)
396
+ if not isinstance(result, Mapping) or result.get("isError") is True or "error" in result:
397
+ return "error", False, None
398
+ status = result.get("status")
399
+ items = result.get("results")
400
+ has_more = result.get("has_more")
401
+ next_cursor = result.get("next_cursor")
402
+ if status not in {"found", "no_match"} or not isinstance(items, list) or not isinstance(has_more, bool):
403
+ return "error", False, None
404
+ if not all(
405
+ isinstance(item, Mapping)
406
+ and set(item) == {"memory_id", "title", "due_date"}
407
+ and isinstance(item.get("memory_id"), str) and bool(item.get("memory_id"))
408
+ and isinstance(item.get("title"), str) and bool(item.get("title"))
409
+ and (item.get("due_date") is None or isinstance(item.get("due_date"), str))
410
+ for item in items
411
+ ):
412
+ return "error", False, None
413
+ if (has_more and not isinstance(next_cursor, str)) or (not has_more and next_cursor is not None):
414
+ return "error", False, None
415
+ if status == "found" and not items:
416
+ return "error", False, None
417
+ if status == "no_match" and items:
418
+ return "error", False, None
419
+ return status, has_more, next_cursor if isinstance(next_cursor, str) else None
420
+
421
+
388
422
  def _handle_codex_post_tool(
389
423
  runtime: HostRuntime,
390
424
  event: Mapping[str, Any],
391
425
  session_id: str | None,
392
426
  turn_id: str | None,
393
427
  ) -> dict[str, Any]:
394
- if not _codex_memleaf_tool(_field(event, "tool_name", "toolName"), "search"):
395
- return {}
396
- if session_id is None or turn_id is None:
428
+ tool_name = _field(event, "tool_name", "toolName")
429
+ is_search = _codex_memleaf_tool(tool_name, "search")
430
+ is_todos = _codex_memleaf_tool(tool_name, "list_todos")
431
+ if not (is_search or is_todos) or session_id is None or turn_id is None:
397
432
  return {}
398
433
  tool_input = _field(event, "tool_input", "toolInput")
399
434
  call_id = _field(event, "tool_use_id", "toolUseId")
400
435
  if not isinstance(tool_input, Mapping) or not isinstance(call_id, str):
401
436
  return {}
402
- runtime.observe_search(
437
+ if is_search:
438
+ runtime.observe_search(
439
+ session_id=session_id,
440
+ turn_id=turn_id,
441
+ status=_codex_search_status(_field(event, "tool_response", "toolResponse")),
442
+ call_id=call_id,
443
+ supplied_retrieval_id=tool_input.get("retrieval_id"),
444
+ )
445
+ return {}
446
+ status, has_more, next_cursor = _codex_todo_result(_field(event, "tool_response", "toolResponse"))
447
+ runtime.observe_todo_list(
403
448
  session_id=session_id,
404
449
  turn_id=turn_id,
405
- status=_codex_search_status(_field(event, "tool_response", "toolResponse")),
450
+ status=status,
406
451
  call_id=call_id,
407
452
  supplied_retrieval_id=tool_input.get("retrieval_id"),
453
+ filter_key=todo_filter_key(tool_input),
454
+ cursor=tool_input.get("cursor") if isinstance(tool_input.get("cursor"), str) else None,
455
+ has_more=has_more,
456
+ next_cursor=next_cursor,
408
457
  )
409
458
  return {}
410
459
 
@@ -28,6 +28,7 @@ from .retrieval_gate import (
28
28
  find_turn,
29
29
  mark_degraded,
30
30
  observe_search,
31
+ observe_todo_list,
31
32
  request_gate_retry,
32
33
  validate_turn,
33
34
  )
@@ -40,9 +41,13 @@ _GATE_RETRY_REASON = (
40
41
  "a no-match result is acceptable."
41
42
  )
42
43
  _GATE_ERROR_REASON = (
43
- "The memleaf search did not complete. Retry the memleaf search once before "
44
+ "The memleaf retrieval did not complete. Retry the memleaf memory tool once before "
44
45
  "answering; do not treat this error as no match."
45
46
  )
47
+ _TODO_PAGINATION_REASON = (
48
+ "The global todo directory is incomplete. Continue memleaf list_todos with the returned "
49
+ "next_cursor until has_more=false before answering."
50
+ )
46
51
 
47
52
 
48
53
  @dataclass(frozen=True)
@@ -250,6 +255,38 @@ class HostRuntime:
250
255
  return False
251
256
  return True
252
257
 
258
+ def observe_todo_list(
259
+ self,
260
+ *,
261
+ session_id: str,
262
+ turn_id: str,
263
+ status: str,
264
+ call_id: str,
265
+ supplied_retrieval_id: Any,
266
+ filter_key: str,
267
+ cursor: str | None,
268
+ has_more: bool,
269
+ next_cursor: str | None,
270
+ ) -> bool:
271
+ retrieval_id = self._retrieval_id(session_id, turn_id)
272
+ if retrieval_id is None or supplied_retrieval_id != retrieval_id or not isinstance(call_id, str) or not call_id:
273
+ return False
274
+ try:
275
+ observe_todo_list(
276
+ self.vault,
277
+ retrieval_id,
278
+ status,
279
+ call_id,
280
+ filter_key=filter_key,
281
+ cursor=cursor,
282
+ has_more=has_more,
283
+ next_cursor=next_cursor,
284
+ current_source=self.host,
285
+ )
286
+ except RetrievalGateError:
287
+ return False
288
+ return True
289
+
253
290
  def complete_turn(
254
291
  self,
255
292
  *,
@@ -277,7 +314,8 @@ class HostRuntime:
277
314
  if gate_state.get("status") == "DEGRADED":
278
315
  degraded = True
279
316
 
280
- if gate_state is not None and gate_state.get("status") in {"NOT_SEARCHED", "ERROR"}:
317
+ todo_pending = gate_state is not None and gate_state.get("todo_list_pending") is True
318
+ if gate_state is not None and (gate_state.get("status") in {"NOT_SEARCHED", "ERROR"} or todo_pending):
281
319
  retries = int(gate_state.get("gate_retries", 0) or 0)
282
320
  if retries < MAX_GATE_RETRIES:
283
321
  try:
@@ -286,7 +324,9 @@ class HostRuntime:
286
324
  pass
287
325
  else:
288
326
  reason = (
289
- _GATE_ERROR_REASON
327
+ _TODO_PAGINATION_REASON
328
+ if todo_pending
329
+ else _GATE_ERROR_REASON
290
330
  if gate_state.get("status") == "ERROR"
291
331
  else _GATE_RETRY_REASON
292
332
  )
@@ -19,14 +19,17 @@ from typing import Any
19
19
  from . import __version__
20
20
  from .adapters.base import update_agents_index
21
21
  from .adapters.codex import CodexAdapter
22
- from .adapters.hermes import HermesAdapter, hermes_home_for_platform
22
+ from .adapters.hermes import (
23
+ HermesAdapter,
24
+ MCP_EXPECTED_TOOL_COUNT,
25
+ hermes_home_for_platform,
26
+ )
23
27
  from .cli import _home_from_environment, _prepare_model_route
24
28
  from .locking import atomic_write_json
25
29
  from .native_registration import ensure_hermes_native_sources
26
30
  from .vault import Vault
27
31
 
28
32
 
29
- _EXPECTED_TOOLS = 11
30
33
  _PROVIDER_VERSION_RE = re.compile(r"^version:\s*([^\s#]+)\s*(?:#.*)?$", re.MULTILINE)
31
34
 
32
35
 
@@ -401,14 +404,14 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
401
404
  "provider": str(provider_path),
402
405
  "model": model,
403
406
  }
404
- if not adapter.test_mcp(detection, expected_tools=_EXPECTED_TOOLS):
407
+ if not adapter.test_mcp(detection, expected_tools=MCP_EXPECTED_TOOL_COUNT):
405
408
  update_agents_index(
406
409
  vault.agents_index_path,
407
410
  {"hermes": {"mcp_status": "failed", "mcp_availability": "unavailable"}},
408
411
  )
409
412
  return {
410
413
  "status": "failure",
411
- "reason": "Hermes MCP test did not confirm 11 tools",
414
+ "reason": f"Hermes MCP test did not confirm {MCP_EXPECTED_TOOL_COUNT} tools",
412
415
  "core_version": core_version,
413
416
  "provider_version": provider_version,
414
417
  "vault": str(vault.root),