memleaf 0.2.19__tar.gz → 0.2.20__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.
- {memleaf-0.2.19 → memleaf-0.2.20}/CHANGELOG.md +8 -0
- {memleaf-0.2.19/src/memleaf.egg-info → memleaf-0.2.20}/PKG-INFO +8 -6
- {memleaf-0.2.19 → memleaf-0.2.20}/README.en.md +5 -2
- {memleaf-0.2.19 → memleaf-0.2.20}/README.md +7 -5
- {memleaf-0.2.19 → memleaf-0.2.20}/pyproject.toml +1 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/adapters/hermes.py +1 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/compaction.py +8 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/hermes_provider/__init__.py +12 -6
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/host_events.py +55 -6
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/host_runtime.py +43 -3
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/mcp_server.py +91 -6
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/memory_writer.py +12 -2
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/models.py +16 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/processing.py +38 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/prompts.py +19 -11
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/retrieval_gate.py +106 -20
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/service.py +162 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/validation.py +86 -2
- {memleaf-0.2.19 → memleaf-0.2.20/src/memleaf.egg-info}/PKG-INFO +8 -6
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf.egg-info/SOURCES.txt +2 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_context_budget.py +2 -2
- memleaf-0.2.20/tests/test_global_todo_acceptance.py +288 -0
- memleaf-0.2.20/tests/test_global_todo_retrieval.py +112 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_pypi_install.py +1 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_retrieval_gate.py +8 -9
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_c1_mcp.py +5 -3
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_c2_init.py +1 -1
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_c3_packaging.py +1 -1
- memleaf-0.2.20/tests/test_v2_gate_limits.py +111 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_v2_mcp_flow.py +9 -10
- memleaf-0.2.19/tests/test_v2_gate_limits.py +0 -207
- {memleaf-0.2.19 → memleaf-0.2.20}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/LICENSE +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/MANIFEST.in +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/examples/README.md +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/examples/basic_usage.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/install.ps1 +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/install.sh +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/setup.cfg +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/config.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/index.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/__init__.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_cross_turn_dedupe_regressions.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_hermes_native_registration.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_hermes_provider.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_host_events.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_install.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_todo_state_recovery.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.19 → memleaf-0.2.20}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.20 — 2026-09-03
|
|
6
|
+
|
|
7
|
+
- Add `list_todos` for complete active todo retrieval across all scopes with stable pagination, date/status filtering, and current-turn retrieval gating.
|
|
8
|
+
- Remove the managed-turn 3-memory / 6000-character aggregate read block while retaining the 2000-character page limit and audit counters.
|
|
9
|
+
- Add first-class `todo.due_date` (`YYYY-MM-DD`) with evidence-grounded date normalization, update/history/compaction propagation, and legacy Markdown compatibility.
|
|
10
|
+
- 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.
|
|
11
|
+
- Keep permanent `knowledge/` globally visible to all sessions and supported Agents sharing the same Vault; provenance fields never filter active memory visibility.
|
|
12
|
+
|
|
5
13
|
## 0.2.19 — 2026-09-03
|
|
6
14
|
|
|
7
15
|
- 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.
|
|
3
|
+
Version: 0.2.20
|
|
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.
|
|
27
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
26
|
+
> **当前版本:0.2.20。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.20 通过 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
|
-
-
|
|
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 能发现
|
|
162
|
+
7. 验证 MCP Server 能发现 12 个工具;
|
|
161
163
|
8. 写入本地 Agent 状态索引。
|
|
162
164
|
|
|
163
165
|
完成后重启 Hermes。
|
|
164
166
|
|
|
165
|
-
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的
|
|
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.
|
|
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.
|
|
7
|
+
> **Version: 0.2.20.**
|
|
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.20 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
|
|
@@ -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.
|
|
@@ -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.
|
|
8
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
7
|
+
> **当前版本:0.2.20。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.20 通过 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
|
-
-
|
|
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 能发现
|
|
143
|
+
7. 验证 MCP Server 能发现 12 个工具;
|
|
142
144
|
8. 写入本地 Agent 状态索引。
|
|
143
145
|
|
|
144
146
|
完成后重启 Hermes。
|
|
145
147
|
|
|
146
|
-
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的
|
|
148
|
+
如果没有检测到 Hermes、无法取得完整模型路由、Provider 激活失败或 MCP 的 12 工具验证失败,安装会明确返回失败,不会把未完成的接入报告为成功。
|
|
147
149
|
|
|
148
150
|
仓库中的 `install.sh` 保留给源码开发、离线源码安装和故障排查;普通 PyPI 用户不需要执行它。
|
|
149
151
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.20"
|
|
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
|
|
@@ -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.
|
|
272
|
-
"when needed
|
|
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)
|
|
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,
|
|
1957
|
-
"
|
|
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")
|
|
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")
|
|
@@ -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 (
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
)
|
|
@@ -28,6 +28,8 @@ from .retrieval_gate import (
|
|
|
28
28
|
begin_turn,
|
|
29
29
|
guarded_read,
|
|
30
30
|
observe_search,
|
|
31
|
+
observe_todo_list,
|
|
32
|
+
todo_filter_key,
|
|
31
33
|
validate_current_turn,
|
|
32
34
|
validate_turn,
|
|
33
35
|
)
|
|
@@ -60,10 +62,11 @@ INSTRUCTIONS = (
|
|
|
60
62
|
"Use read(memory_id) on the best matching entry before relying on a past fact. Read more only "
|
|
61
63
|
"when needed, not every entry to filter unrelated items. You may refine the query/scope and "
|
|
62
64
|
"search again. Pass the host-provided retrieval_id to search and read; never invent or replace "
|
|
63
|
-
"it
|
|
64
|
-
"
|
|
65
|
-
"and
|
|
66
|
-
"
|
|
65
|
+
"it. Managed reads have no aggregate ID/character quota; read every relevant memory needed for "
|
|
66
|
+
"the user's question while keeping each read page at 2000 characters. MCP read requires retrieval_id "
|
|
67
|
+
"and a current FOUND search or list_todos result; NO_MATCH, ERROR, and DEGRADED turns cannot read. "
|
|
68
|
+
"For global current-todo questions use list_todos rather than relevance search, omit scope to cover "
|
|
69
|
+
"all scopes, follow next_cursor until has_more=false, then read every matching todo body. "
|
|
67
70
|
"Legacy context is an explicit compatibility interface, not an alternative to this "
|
|
68
71
|
"scope/search/read flow. Managed MCP search is directory-only and rejects view=full; "
|
|
69
72
|
"Python full-result search remains compatible. Without host integration, tools remain an explicit fallback only; "
|
|
@@ -205,6 +208,28 @@ _TOOLS: tuple[dict[str, Any], ...] = (
|
|
|
205
208
|
required=["query", "retrieval_id"],
|
|
206
209
|
),
|
|
207
210
|
},
|
|
211
|
+
{
|
|
212
|
+
"name": "list_todos",
|
|
213
|
+
"description": (
|
|
214
|
+
"Enumerate current memleaf todo memories by status/date across all scopes by default. "
|
|
215
|
+
"This is not relevance search. Continue with next_cursor until has_more=false, then read "
|
|
216
|
+
"the matching todo bodies with the same retrieval_id."
|
|
217
|
+
),
|
|
218
|
+
"inputSchema": _object_schema(
|
|
219
|
+
{
|
|
220
|
+
"status": {"type": "string", "enum": ["active", "completed", "cancelled", "all"]},
|
|
221
|
+
"scope": _text_or_texts_schema(),
|
|
222
|
+
"due_from": {"type": "string"},
|
|
223
|
+
"due_to": {"type": "string"},
|
|
224
|
+
"include_overdue": {"type": "boolean"},
|
|
225
|
+
"include_unscheduled": {"type": "boolean"},
|
|
226
|
+
"cursor": {"type": "string"},
|
|
227
|
+
"limit": {"type": "integer", "minimum": 1},
|
|
228
|
+
"retrieval_id": {"type": "string"},
|
|
229
|
+
},
|
|
230
|
+
required=["retrieval_id"],
|
|
231
|
+
),
|
|
232
|
+
},
|
|
208
233
|
{
|
|
209
234
|
"name": "read",
|
|
210
235
|
"description": (
|
|
@@ -439,7 +464,23 @@ def _read_page_result(value: Any) -> dict[str, Any] | None:
|
|
|
439
464
|
raise ValueError("invalid read continuation")
|
|
440
465
|
elif value["next_offset"] is not None or end != value["total_chars"]:
|
|
441
466
|
raise ValueError("invalid read final page")
|
|
442
|
-
|
|
467
|
+
result = {name: _jsonable(value[name]) for name in fields if name in value}
|
|
468
|
+
memory_type = value.get("type")
|
|
469
|
+
status = value.get("status")
|
|
470
|
+
due_date = value.get("due_date")
|
|
471
|
+
if memory_type is not None and not isinstance(memory_type, str):
|
|
472
|
+
raise ValueError("invalid read memory type")
|
|
473
|
+
if status is not None and not isinstance(status, str):
|
|
474
|
+
raise ValueError("invalid read todo status")
|
|
475
|
+
if due_date is not None and not isinstance(due_date, str):
|
|
476
|
+
raise ValueError("invalid read todo due date")
|
|
477
|
+
if "type" in value:
|
|
478
|
+
result["type"] = memory_type
|
|
479
|
+
if "status" in value:
|
|
480
|
+
result["status"] = status
|
|
481
|
+
if "due_date" in value:
|
|
482
|
+
result["due_date"] = due_date
|
|
483
|
+
return result
|
|
443
484
|
|
|
444
485
|
|
|
445
486
|
def _catalog_result(value: Any) -> dict[str, Any]:
|
|
@@ -703,6 +744,34 @@ def _observe_mcp_search(
|
|
|
703
744
|
)
|
|
704
745
|
|
|
705
746
|
|
|
747
|
+
|
|
748
|
+
def _observe_mcp_todos(
|
|
749
|
+
service: Memleaf,
|
|
750
|
+
state: Mapping[str, Any] | None,
|
|
751
|
+
value: Mapping[str, Any] | None,
|
|
752
|
+
request_id: Any,
|
|
753
|
+
arguments: Mapping[str, Any],
|
|
754
|
+
) -> None:
|
|
755
|
+
if state is None:
|
|
756
|
+
return
|
|
757
|
+
status = value.get("status") if isinstance(value, Mapping) else "error"
|
|
758
|
+
if status not in {"found", "no_match"}:
|
|
759
|
+
status = "error"
|
|
760
|
+
has_more = value.get("has_more") if isinstance(value, Mapping) else False
|
|
761
|
+
next_cursor = value.get("next_cursor") if isinstance(value, Mapping) else None
|
|
762
|
+
observe_todo_list(
|
|
763
|
+
service.vault,
|
|
764
|
+
state["retrieval_id"],
|
|
765
|
+
status,
|
|
766
|
+
_mcp_search_call_id(request_id),
|
|
767
|
+
filter_key=todo_filter_key(arguments),
|
|
768
|
+
cursor=arguments.get("cursor") if isinstance(arguments.get("cursor"), str) else None,
|
|
769
|
+
has_more=has_more if isinstance(has_more, bool) else False,
|
|
770
|
+
next_cursor=next_cursor if isinstance(next_cursor, str) else None,
|
|
771
|
+
current_source="hermes",
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
|
|
706
775
|
def _invoke_tool(
|
|
707
776
|
service: Memleaf,
|
|
708
777
|
name: str,
|
|
@@ -712,7 +781,7 @@ def _invoke_tool(
|
|
|
712
781
|
) -> dict[str, Any]:
|
|
713
782
|
if name not in _TOOL_BY_NAME:
|
|
714
783
|
raise _InvalidParams
|
|
715
|
-
if name in {"read", "search"} and (
|
|
784
|
+
if name in {"read", "search", "list_todos"} and (
|
|
716
785
|
not isinstance(arguments, dict) or "retrieval_id" not in arguments
|
|
717
786
|
):
|
|
718
787
|
return _tool_error(RetrievalGateError("retrieval_id_required"))
|
|
@@ -773,6 +842,22 @@ def _invoke_tool(
|
|
|
773
842
|
except Exception as error:
|
|
774
843
|
# A managed result is not successful until the gate records it.
|
|
775
844
|
return _tool_error(error)
|
|
845
|
+
elif name == "list_todos":
|
|
846
|
+
retrieval_id = args.pop("retrieval_id", None)
|
|
847
|
+
managed_state = _managed_search_state(service, retrieval_id)
|
|
848
|
+
observed_args = dict(args)
|
|
849
|
+
try:
|
|
850
|
+
value = service.list_todos(**args)
|
|
851
|
+
except Exception as error:
|
|
852
|
+
try:
|
|
853
|
+
_observe_mcp_todos(service, managed_state, None, request_id, observed_args)
|
|
854
|
+
except Exception as observe_error:
|
|
855
|
+
return _tool_error(observe_error)
|
|
856
|
+
return _tool_error(error)
|
|
857
|
+
try:
|
|
858
|
+
_observe_mcp_todos(service, managed_state, value, request_id, observed_args)
|
|
859
|
+
except Exception as error:
|
|
860
|
+
return _tool_error(error)
|
|
776
861
|
elif name == "read":
|
|
777
862
|
# Keep the protocol boundary hard-capped even if a caller sends a
|
|
778
863
|
# larger value. Core read_page performs the same authoritative
|
|
@@ -82,6 +82,7 @@ class MemoryWriter:
|
|
|
82
82
|
and (ignore_sources or left.sources == right.sources)
|
|
83
83
|
and left.status == right.status
|
|
84
84
|
and left.completed_at == right.completed_at
|
|
85
|
+
and left.due_date == right.due_date
|
|
85
86
|
and left_extra == right_extra
|
|
86
87
|
)
|
|
87
88
|
|
|
@@ -263,9 +264,15 @@ class MemoryWriter:
|
|
|
263
264
|
summary = request["summary"]
|
|
264
265
|
memory_id = existing.memory_id if existing is not None else request["memory_id"]
|
|
265
266
|
created = existing.created if existing is not None else now
|
|
266
|
-
status = summary.get("status")
|
|
267
|
+
status = summary.get("status") if "status" in summary else (existing.status if existing is not None else None)
|
|
267
268
|
if summary["type"] == "todo" and status is None:
|
|
268
269
|
status = "active"
|
|
270
|
+
due_date = summary.get("due_date") if "due_date" in summary else (existing.due_date if existing is not None else None)
|
|
271
|
+
completed_at = (
|
|
272
|
+
summary.get("completed_at")
|
|
273
|
+
if "completed_at" in summary
|
|
274
|
+
else existing.completed_at if existing is not None and status == "completed" else None
|
|
275
|
+
)
|
|
269
276
|
extra = dict(existing.extra) if existing is not None else {}
|
|
270
277
|
extra["source"] = request["turn"].source
|
|
271
278
|
if request.get("explicit_remember") is True:
|
|
@@ -287,7 +294,8 @@ class MemoryWriter:
|
|
|
287
294
|
hit_count=existing.hit_count if existing is not None else 0,
|
|
288
295
|
last_hit_at=existing.last_hit_at if existing is not None else None,
|
|
289
296
|
status=status,
|
|
290
|
-
completed_at=
|
|
297
|
+
completed_at=completed_at,
|
|
298
|
+
due_date=due_date,
|
|
291
299
|
extra=extra,
|
|
292
300
|
)
|
|
293
301
|
|
|
@@ -315,6 +323,7 @@ class MemoryWriter:
|
|
|
315
323
|
last_hit_at=existing.last_hit_at,
|
|
316
324
|
status=existing.status,
|
|
317
325
|
completed_at=existing.completed_at,
|
|
326
|
+
due_date=existing.due_date,
|
|
318
327
|
extra=dict(existing.extra),
|
|
319
328
|
)
|
|
320
329
|
|
|
@@ -356,6 +365,7 @@ class MemoryWriter:
|
|
|
356
365
|
last_hit_at=old.last_hit_at,
|
|
357
366
|
status=old.status,
|
|
358
367
|
completed_at=old.completed_at,
|
|
368
|
+
due_date=old.due_date,
|
|
359
369
|
extra=extra,
|
|
360
370
|
)
|
|
361
371
|
path = self.service.vault.memory_path(history_id, "history")
|