memleaf 0.1.7__tar.gz → 0.2.0__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 (102) hide show
  1. {memleaf-0.1.7 → memleaf-0.2.0}/CHANGELOG.md +16 -0
  2. {memleaf-0.1.7 → memleaf-0.2.0}/PKG-INFO +12 -4
  3. {memleaf-0.1.7 → memleaf-0.2.0}/README.en.md +11 -3
  4. {memleaf-0.1.7 → memleaf-0.2.0}/README.md +11 -3
  5. {memleaf-0.1.7 → memleaf-0.2.0}/RELEASE_CHECKLIST.md +19 -0
  6. {memleaf-0.1.7 → memleaf-0.2.0}/pyproject.toml +1 -1
  7. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/__init__.py +1 -1
  8. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/adapters/base.py +25 -1
  9. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/adapters/codex.py +48 -8
  10. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/cli.py +29 -8
  11. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/hermes_provider/__init__.py +2 -0
  12. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  13. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/host_events.py +4 -0
  14. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/installer.py +13 -2
  15. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/mcp_server.py +26 -10
  16. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/service.py +3 -3
  17. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf.egg-info/PKG-INFO +12 -4
  18. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf.egg-info/SOURCES.txt +2 -0
  19. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_codex_install.py +86 -4
  20. memleaf-0.2.0/tests/test_codex_native_cli.py +90 -0
  21. memleaf-0.2.0/tests/test_cross_host_acceptance.py +138 -0
  22. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_host_events.py +40 -0
  23. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_pypi_install.py +48 -2
  24. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_retrieval_v2.py +3 -2
  25. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_session_lineage.py +18 -10
  26. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_c1_mcp.py +5 -2
  27. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_c3_packaging.py +1 -1
  28. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_v2_host_flow.py +1 -1
  29. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_v2_mcp_flow.py +4 -4
  30. {memleaf-0.1.7 → memleaf-0.2.0}/IMPLEMENTATION_PLAN.md +0 -0
  31. {memleaf-0.1.7 → memleaf-0.2.0}/LICENSE +0 -0
  32. {memleaf-0.1.7 → memleaf-0.2.0}/MANIFEST.in +0 -0
  33. {memleaf-0.1.7 → memleaf-0.2.0}/examples/README.md +0 -0
  34. {memleaf-0.1.7 → memleaf-0.2.0}/examples/basic_usage.py +0 -0
  35. {memleaf-0.1.7 → memleaf-0.2.0}/examples/mcp_stdio.ndjson +0 -0
  36. {memleaf-0.1.7 → memleaf-0.2.0}/install.ps1 +0 -0
  37. {memleaf-0.1.7 → memleaf-0.2.0}/install.sh +0 -0
  38. {memleaf-0.1.7 → memleaf-0.2.0}/setup.cfg +0 -0
  39. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/__main__.py +0 -0
  40. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/adapters/__init__.py +0 -0
  41. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/adapters/antigravity.py +0 -0
  42. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/adapters/hermes.py +0 -0
  43. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/budget.py +0 -0
  44. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/capture.py +0 -0
  45. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/compaction.py +0 -0
  46. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/config.py +0 -0
  47. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/credentials.py +0 -0
  48. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/frontmatter.py +0 -0
  49. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/hermes_provider/README.md +0 -0
  50. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/host_runtime.py +0 -0
  51. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/inbox.py +0 -0
  52. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/index.py +0 -0
  53. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/llm/__init__.py +0 -0
  54. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/llm/base.py +0 -0
  55. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/llm/claude_compatible.py +0 -0
  56. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/llm/gemini.py +0 -0
  57. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/llm/openai_compatible.py +0 -0
  58. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/llm/router.py +0 -0
  59. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/locking.py +0 -0
  60. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/memory_writer.py +0 -0
  61. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/model_discovery.py +0 -0
  62. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/models.py +0 -0
  63. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/native_index.py +0 -0
  64. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/processing.py +0 -0
  65. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/prompts.py +0 -0
  66. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/redaction.py +0 -0
  67. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/retrieval.py +0 -0
  68. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/retrieval_gate.py +0 -0
  69. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/scope_maintenance.py +0 -0
  70. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/scope_state.py +0 -0
  71. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/validation.py +0 -0
  72. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf/vault.py +0 -0
  73. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf.egg-info/dependency_links.txt +0 -0
  74. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf.egg-info/entry_points.txt +0 -0
  75. {memleaf-0.1.7 → memleaf-0.2.0}/src/memleaf.egg-info/top_level.txt +0 -0
  76. {memleaf-0.1.7 → memleaf-0.2.0}/tests/__init__.py +0 -0
  77. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_admission_noise.py +0 -0
  78. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_context_budget.py +0 -0
  79. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_credential_safety.py +0 -0
  80. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_hermes_provider.py +0 -0
  81. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_hermes_stdio_transport.py +0 -0
  82. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_host_runtime_contract.py +0 -0
  83. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_install.py +0 -0
  84. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_maintenance_v2.py +0 -0
  85. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_model_discovery.py +0 -0
  86. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_retrieval_gate.py +0 -0
  87. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_a.py +0 -0
  88. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b1.py +0 -0
  89. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b2a.py +0 -0
  90. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b2b.py +0 -0
  91. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3a_commit.py +0 -0
  92. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3a_contract.py +0 -0
  93. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3b_native_context.py +0 -0
  94. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3b_native_index.py +0 -0
  95. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3b_scope.py +0 -0
  96. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3c_retrieval.py +0 -0
  97. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  98. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_stage_c2_init.py +0 -0
  99. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_upgrade_preserves_vault.py +0 -0
  100. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_v2_gate_limits.py +0 -0
  101. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_v2_nomatch_semantics.py +0 -0
  102. {memleaf-0.1.7 → memleaf-0.2.0}/tests/test_v2_search_gate_acceptance.py +0 -0
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.0 — 2026-08-31
6
+
7
+ - Hardened the supported Codex integration against the official Codex CLI 0.151.0 on Windows and macOS, including `CODEX_HOME`, npm `codex.cmd` discovery, UTF-8 CLI output, idempotent `mcp add/get`, and quote-free Windows lifecycle commands carried through a UTF-16LE PowerShell `EncodedCommand` payload.
8
+ - Aligned Codex `PreToolUse` argument rewriting with the current hook contract by returning `permissionDecision=allow` together with `updatedInput`, preserving the current-turn `retrieval_id` binding.
9
+ - Restored the finalized automatic retrieval contract: the bounded Scope Map exposes only Scope metadata, `search` candidates expose only `memory_id + title`, and selected memory bodies enter context only through bounded `read` calls under the existing retrieval gate and budgets.
10
+ - Added durable shared-Vault acceptance for Codex → Codex, Hermes → Codex, and Codex → Hermes so cross-session and cross-host memory reuse is verified without duplicating CREATE/UPDATE/NO_CHANGE, history, deduplication, or processing semantics outside the Core.
11
+ - Kept Codex installation explicitly opt-in and fail-closed: existing Codex model/provider, profiles, sandbox/approval settings, MCP entries, hooks, and custom providers remain untouched; conflicting host Vaults are rejected rather than guessed.
12
+ - Decoupled the Codex host from the extraction model. Automatic processing uses an independent memleaf Model Route, reports `processing_status=model_route_required` when no complete route exists, and never copies Codex credentials or silently consumes the active Codex session model.
13
+ - Promoted native Codex Windows/macOS acceptance, cross-host memory roundtrips, Linux/Windows/macOS host tests, and packaging checks into release-blocking CI gates.
14
+
15
+ ## 0.1.8 — 2026-08-31
16
+
17
+ - Fixed Windows UTF-8 stdio handling across the MCP server, Hermes MemoryProvider, and Codex host-event entry point so Chinese and other non-ASCII memory content can be captured, searched, read, and injected without depending on the system code page.
18
+ - Added native Windows coverage for Codex executable discovery and Hermes MCP subprocess execution, including Unicode paths and payloads.
19
+ - Strengthened Windows and macOS release gates to exercise Hermes and Codex installation, host lifecycle, bounded Scope Map injection, controlled `search` → `read`, retrieval budgets, memory extraction/update semantics, and session-lineage behavior before release.
20
+
5
21
  ## 0.1.7 — 2026-08-31
6
22
 
7
23
  - Added supported Codex integration through the explicit
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.1.7
3
+ Version: 0.2.0
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.1.7。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.1.7 通过 PyPI 分发。
26
+ > **当前版本:0.2.0。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.0 通过 PyPI 分发。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
30
30
  ## 项目定位
@@ -65,7 +65,7 @@ memleaf 不会把整个 Vault 或整段历史对话自动塞进模型上下文
65
65
 
66
66
  1. Agent 使用当前完整会话和 Scope Map 选择检索范围与查询词;
67
67
  2. 至少调用一次 `search`;
68
- 3. `search` 只返回候选的 `memory_id`、标题和 Scope,不直接返回正文;
68
+ 3. `search` 候选默认只返回 `memory_id` 和标题;Scope 已由 Scope Map 与本次搜索参数确定,不随每条候选重复返回,也不直接返回正文;
69
69
  4. 只有需要引用事实时,才使用同一轮返回的 `retrieval_id` 调用 `read`;
70
70
  5. 根据读取到的关键记忆回答,不把所有候选全部读完。
71
71
 
@@ -172,6 +172,14 @@ python -m pip install -U memleaf && python -m memleaf install --host codex
172
172
 
173
173
  安装后打开 Codex,运行 `/hooks`,审核并信任 memleaf Hook。完成授权前,MCP 可用不等于自动捕获、检索门控和提炼已经激活;安装结果会明确显示 `pending_user_review`,不会把待审核状态报告为已启用。
174
174
 
175
+ Codex 只作为宿主,不作为 memleaf 的提炼模型来源。已有的独立 memleaf Model Route 会继续复用;如果当前 Vault 还没有完整 Model Route,Codex 安装仍可完成 MCP/Hook 配置,但结果会明确返回 `processing_status=model_route_required`,在配置模型前不能把“自动记忆提炼”视为已就绪。memleaf 不读取、不复制也不修改 Codex 的 `model`、`model_provider`、`base_url` 或认证信息,也不会为了提炼偷偷消耗 Codex 会话额度。Codex-only 用户可针对同一个 Vault 运行:
176
+
177
+ ```bash
178
+ python -m memleaf init --no-hermes --vault /path/to/the/same/vault
179
+ ```
180
+
181
+ 完成独立 Model Route 配置后,再使用 Codex 自动提炼。DeepSeek、OpenRouter 或其他自定义 Codex provider 因此保持原样。
182
+
175
183
  Antigravity(反重力)当前不支持,安装流程不会检测、安装或修改其配置。
176
184
 
177
185
  ### 高级初始化命令(可选)
@@ -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.1.7.**
8
- > The core library, Vault, stdio MCP server, initialization CLI, model routing, memory extraction, controlled retrieval protocol, and host adapters are implemented. memleaf 0.1.7 is distributed through PyPI.
7
+ > **Version: 0.2.0.**
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.0 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
@@ -46,7 +46,7 @@ For a normal user message, the intended flow is:
46
46
 
47
47
  1. The Agent uses the full current conversation and the Scope Map to choose a scope and query.
48
48
  2. It calls `search` at least once.
49
- 3. `search` returns only candidate `memory_id`, title, and Scope metadata; it does not return the body.
49
+ 3. `search` candidates return only `memory_id` and title by default. Scope is already selected through the Scope Map and the search input, so it is not repeated per candidate; bodies are never returned by search.
50
50
  4. If a business fact is needed, the Agent calls `read` with the `retrieval_id` from the same turn.
51
51
  5. The Agent answers from the selected bodies instead of reading every candidate.
52
52
 
@@ -153,6 +153,14 @@ This command reuses the one Vault already configured for Hermes or Codex, or cre
153
153
 
154
154
  After installation, open Codex, run `/hooks`, and review and trust the memleaf hooks. Until that approval is complete, an available MCP server does not mean that automatic capture, retrieval gating, and processing are active. The installer reports `pending_user_review` instead of claiming that unreviewed hooks are enabled.
155
155
 
156
+ Codex is a host, not memleaf's extraction-model source. An existing independent memleaf Model Route is reused. If the selected Vault has no complete Model Route yet, Codex MCP/hooks can still be configured, but installation explicitly returns `processing_status=model_route_required`; automatic memory extraction is not ready until that route is configured. memleaf does not read, copy, or modify Codex `model`, `model_provider`, `base_url`, or credentials, and it does not silently spend Codex session quota for extraction. A Codex-only user can configure the same Vault with:
157
+
158
+ ```bash
159
+ python -m memleaf init --no-hermes --vault /path/to/the/same/vault
160
+ ```
161
+
162
+ After the independent Model Route is ready, Codex automatic extraction can use it. DeepSeek, OpenRouter, and other custom Codex providers therefore remain untouched.
163
+
156
164
  Antigravity is not currently supported; the installer does not detect, install, or modify its configuration.
157
165
 
158
166
  ### Advanced initialization commands (optional)
@@ -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.1.7。**
8
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.1.7 通过 PyPI 分发。
7
+ > **当前版本:0.2.0。**
8
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.0 通过 PyPI 分发。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
11
11
  ## 项目定位
@@ -46,7 +46,7 @@ memleaf 不会把整个 Vault 或整段历史对话自动塞进模型上下文
46
46
 
47
47
  1. Agent 使用当前完整会话和 Scope Map 选择检索范围与查询词;
48
48
  2. 至少调用一次 `search`;
49
- 3. `search` 只返回候选的 `memory_id`、标题和 Scope,不直接返回正文;
49
+ 3. `search` 候选默认只返回 `memory_id` 和标题;Scope 已由 Scope Map 与本次搜索参数确定,不随每条候选重复返回,也不直接返回正文;
50
50
  4. 只有需要引用事实时,才使用同一轮返回的 `retrieval_id` 调用 `read`;
51
51
  5. 根据读取到的关键记忆回答,不把所有候选全部读完。
52
52
 
@@ -153,6 +153,14 @@ python -m pip install -U memleaf && python -m memleaf install --host codex
153
153
 
154
154
  安装后打开 Codex,运行 `/hooks`,审核并信任 memleaf Hook。完成授权前,MCP 可用不等于自动捕获、检索门控和提炼已经激活;安装结果会明确显示 `pending_user_review`,不会把待审核状态报告为已启用。
155
155
 
156
+ Codex 只作为宿主,不作为 memleaf 的提炼模型来源。已有的独立 memleaf Model Route 会继续复用;如果当前 Vault 还没有完整 Model Route,Codex 安装仍可完成 MCP/Hook 配置,但结果会明确返回 `processing_status=model_route_required`,在配置模型前不能把“自动记忆提炼”视为已就绪。memleaf 不读取、不复制也不修改 Codex 的 `model`、`model_provider`、`base_url` 或认证信息,也不会为了提炼偷偷消耗 Codex 会话额度。Codex-only 用户可针对同一个 Vault 运行:
157
+
158
+ ```bash
159
+ python -m memleaf init --no-hermes --vault /path/to/the/same/vault
160
+ ```
161
+
162
+ 完成独立 Model Route 配置后,再使用 Codex 自动提炼。DeepSeek、OpenRouter 或其他自定义 Codex provider 因此保持原样。
163
+
156
164
  Antigravity(反重力)当前不支持,安装流程不会检测、安装或修改其配置。
157
165
 
158
166
  ### 高级初始化命令(可选)
@@ -21,8 +21,20 @@
21
21
 
22
22
  - [ ] Linux Python 3.11 / 3.12 / 3.13 完整 unittest 通过。
23
23
  - [ ] Windows Python 3.11 / 3.12 / 3.13 安装与集成验收通过。
24
+ - [ ] macOS Python 3.11 / 3.13 Hermes/Codex 宿主与记忆语义验收通过。
24
25
  - [ ] Windows 真实 Provider → `memleaf-mcp` stdio 回归通过:
25
26
  `stats`、`scope_catalog`、user/assistant capture、inbox 落盘。
27
+ - [ ] Windows 与 macOS 使用当前受支持 Codex CLI 做真实 `codex mcp add/get` 验收,
28
+ Unicode/空格路径正常,重复安装为 `already_configured` 且 `changed=false`。
29
+ - [ ] Codex `PreToolUse` 参数重写符合当前 Hook 合同:
30
+ `permissionDecision=allow` 与 `updatedInput` 同时存在,当前轮 `retrieval_id` 不丢失。
31
+ - [ ] Windows `commandWindows` 不含会触发 Codex 当前 cmd 外层引号缺陷的内嵌双引号;
32
+ Python/Vault 含空格和中文时,quote-free 命令在原生 `cmd.exe` 执行回归通过。
33
+ - [ ] Codex → Codex、Hermes → Codex、Codex → Hermes 三条共享 Vault 持久记忆闭环全部通过。
34
+ - [ ] Scope Map 仍只注入 Scope/alias/检索协议;`search` 候选严格只返回
35
+ `memory_id + title`,`read` 才读取选中正文,检索预算与 retrieval gate 保持。
36
+ - [ ] CREATE / UPDATE / NO_CHANGE、history、去重、process 继续由 Core 统一实现,
37
+ Codex/Hermes Adapter 不复制记忆语义。
26
38
  - [ ] wheel 与 sdist 构建通过。
27
39
  - [ ] 从 wheel 安装后 `memleaf` / `memleaf-mcp` 入口正常。
28
40
  - [ ] 从 sdist 解包后的测试通过。
@@ -48,6 +60,13 @@
48
60
  - [ ] Windows 路径仍支持 `%LOCALAPPDATA%\hermes` 及官方 launcher 布局。
49
61
  - [ ] 从旧版本升级时保留已有 Hermes `memleaf.json` 中的自定义 Vault,不静默切换到默认 Vault。
50
62
  - [ ] Provider 的 Windows stdio 实现不重新引入 `select.select(pipe)`。
63
+ - [ ] 普通升级/`memleaf install` 不自动配置 Codex;只有显式
64
+ `memleaf install --host codex` 才修改 Codex memleaf 接入。
65
+ - [ ] Codex 安装前后的 model/model_provider/base_url、sandbox、approval、
66
+ profiles、已有 MCP 与已有 Hooks 保持;DeepSeek/OpenRouter/自定义 provider 不被改写。
67
+ - [ ] Hermes/Codex 已有 Vault 唯一一致时复用;冲突时 `vault_conflict` fail closed。
68
+ - [ ] Codex 自动提炼只使用独立 memleaf Model Route;没有 Route 时明确返回
69
+ `processing_status=model_route_required`,不复制 Codex credential、不隐式消耗 Codex 会话模型。
51
70
  - [ ] shell、Python 与 PowerShell 安装路径都使用
52
71
  `src/memleaf/hermes_provider` 这一份权威 Provider 源码。
53
72
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.1.7"
7
+ version = "0.2.0"
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.1.7"
3
+ __version__ = "0.2.0"
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
@@ -7,6 +7,7 @@ contents and possible secrets cannot accidentally end up in ``agents.json``.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import base64
10
11
  import datetime as _datetime
11
12
  import hashlib
12
13
  import inspect
@@ -355,10 +356,28 @@ def host_event_command(
355
356
  absolute_vault(vault),
356
357
  )
357
358
  if (os.name if platform is None else platform) == "nt":
358
- return subprocess.list2cmdline(args)
359
+ # Codex currently wraps commandWindows in an outer cmd.exe /C quoted
360
+ # string. Embedded quotes can break that wrapper. Put the real argv in
361
+ # a UTF-16LE PowerShell EncodedCommand payload instead: commandWindows
362
+ # itself stays quote-free while paths with spaces/Unicode remain exact.
363
+ script = "& " + " ".join(_powershell_single_quoted(argument) for argument in args)
364
+ script += "; exit $LASTEXITCODE"
365
+ payload = base64.b64encode(script.encode("utf-16le")).decode("ascii")
366
+ return (
367
+ "powershell.exe -NoLogo -NoProfile -NonInteractive "
368
+ "-ExecutionPolicy Bypass -EncodedCommand " + payload
369
+ )
359
370
  return " ".join(shlex.quote(argument) for argument in args)
360
371
 
361
372
 
373
+ def _powershell_single_quoted(value: str) -> str:
374
+ """Return one literal PowerShell string for an encoded Windows hook."""
375
+
376
+ if not isinstance(value, str) or not value or any(char in value for char in "\r\n\x00"):
377
+ raise ValueError("invalid Windows command argument")
378
+ return "'" + value.replace("'", "''") + "'"
379
+
380
+
362
381
  def run_argv(
363
382
  runner: CommandRunner | None,
364
383
  argv: Sequence[str],
@@ -427,6 +446,11 @@ def _subprocess_runner(
427
446
  "check": False,
428
447
  "capture_output": True,
429
448
  "text": True,
449
+ # Codex and the supported host CLIs emit UTF-8. Relying on the
450
+ # Windows process default code page corrupts non-ASCII JSON paths
451
+ # before adapters can validate them.
452
+ "encoding": "utf-8",
453
+ "errors": "strict",
430
454
  "env": dict(env),
431
455
  }
432
456
  if input_text is not None:
@@ -68,6 +68,12 @@ class CodexAdapter:
68
68
  self.env = adapter_environment(env)
69
69
  effective_home = home if home is not None else self.env.get("HOME")
70
70
  self.home = adapter_home(effective_home)
71
+ codex_home_value = self.env.get("CODEX_HOME")
72
+ self.codex_home = (
73
+ Path(codex_home_value).expanduser().resolve()
74
+ if isinstance(codex_home_value, str) and codex_home_value.strip()
75
+ else self.home / ".codex"
76
+ )
71
77
  self.platform = os.name if platform is None else platform
72
78
  self.interpreter = interpreter if interpreter is not None else sys.executable
73
79
  if known_paths is None:
@@ -101,11 +107,11 @@ class CodexAdapter:
101
107
 
102
108
  @property
103
109
  def config_path(self) -> Path:
104
- return self.home / ".codex" / "config.toml"
110
+ return self.codex_home / "config.toml"
105
111
 
106
112
  @property
107
113
  def hooks_path(self) -> Path:
108
- return self.home / ".codex" / "hooks.json"
114
+ return self.codex_home / "hooks.json"
109
115
 
110
116
  def detect(self) -> Detection:
111
117
  config = self.config_path
@@ -124,11 +130,20 @@ class CodexAdapter:
124
130
  status="diagnostic",
125
131
  )
126
132
  if executable is None:
127
- executable = resolve_executable(
128
- "codex.exe" if self.platform == "nt" else "codex",
129
- env=self.env,
130
- known_paths=self.known_paths,
131
- )
133
+ # Native Windows installs commonly expose codex.exe, while the
134
+ # official npm package exposes codex.cmd on PATH. Accept both
135
+ # before falling back to documented bundled/runtime locations.
136
+ path_names = ("codex.exe", "codex.cmd", "codex") if self.platform == "nt" else ("codex",)
137
+ for name in path_names:
138
+ executable = resolve_executable(name, env=self.env)
139
+ if executable is not None:
140
+ break
141
+ if executable is None:
142
+ executable = resolve_executable(
143
+ "codex.exe" if self.platform == "nt" else "codex",
144
+ env={**self.env, "PATH": ""},
145
+ known_paths=self.known_paths,
146
+ )
132
147
  config_value = str(config)
133
148
  if executable is not None:
134
149
  return Detection(
@@ -584,6 +599,22 @@ def _entry_vault(entry: Mapping[str, Any]) -> Path | None:
584
599
  return Path(raw_vault).expanduser().resolve()
585
600
 
586
601
 
602
+ def _path_equivalent(left: str, right: str) -> bool:
603
+ """Compare host paths without weakening the expected command structure."""
604
+
605
+ try:
606
+ if Path(left).exists() and Path(right).exists():
607
+ return os.path.samefile(left, right)
608
+ except OSError:
609
+ pass
610
+ try:
611
+ left_value = os.path.normcase(str(Path(left).expanduser().resolve()))
612
+ right_value = os.path.normcase(str(Path(right).expanduser().resolve()))
613
+ except (OSError, RuntimeError):
614
+ return False
615
+ return left_value == right_value
616
+
617
+
587
618
  def _entry_matches(
588
619
  entry: Mapping[str, Any],
589
620
  vault: Path | str,
@@ -593,8 +624,17 @@ def _entry_matches(
593
624
  candidate = _entry_candidate(entry)
594
625
  if candidate is None:
595
626
  return False
627
+ command = candidate.get("command")
628
+ args = candidate.get("args")
629
+ if not isinstance(command, str) or not isinstance(args, list) or len(args) != 4:
630
+ return False
631
+ if args[:3] != ["-m", "memleaf.mcp_server", "--vault"]:
632
+ return False
633
+ raw_vault = args[3]
634
+ if not isinstance(raw_vault, str) or not raw_vault:
635
+ return False
596
636
  expected = mcp_command(vault, interpreter=interpreter)
597
- return candidate.get("command") == expected[0] and candidate.get("args") == expected[1:]
637
+ return _path_equivalent(command, expected[0]) and _path_equivalent(raw_vault, expected[4])
598
638
 
599
639
 
600
640
  def _inline_hooks_diagnostic(path: Path) -> str | None:
@@ -47,7 +47,7 @@ def build_parser() -> argparse.ArgumentParser:
47
47
  init.add_argument(
48
48
  "--no-antigravity",
49
49
  action="store_true",
50
- help="accepted for compatibility; Antigravity is disabled in v0.1",
50
+ help="accepted for compatibility; Antigravity is currently unsupported",
51
51
  )
52
52
  init.add_argument(
53
53
  "--no-model-discovery",
@@ -76,7 +76,19 @@ def build_parser() -> argparse.ArgumentParser:
76
76
  return parser
77
77
 
78
78
 
79
+ def _configure_stdio_utf8() -> None:
80
+ for stream in (sys.stdin, sys.stdout, sys.stderr):
81
+ reconfigure = getattr(stream, "reconfigure", None)
82
+ if not callable(reconfigure):
83
+ continue
84
+ try:
85
+ reconfigure(encoding="utf-8", errors="strict")
86
+ except (OSError, ValueError):
87
+ continue
88
+
89
+
79
90
  def main(argv: Sequence[str] | None = None) -> int:
91
+ _configure_stdio_utf8()
80
92
  parser = build_parser()
81
93
  args = parser.parse_args(argv)
82
94
  try:
@@ -200,13 +212,22 @@ def _init(args: argparse.Namespace) -> dict:
200
212
  attempt=args.all,
201
213
  )
202
214
 
203
- # Retain legacy APIs, but never inspect or configure unsupported hosts.
204
- # Clear stale activation claims in our own index, not the user's hosts.
205
- for name, config_path in (
206
- ("codex", home / ".codex" / "config.toml"),
207
- ("antigravity", home / ".gemini" / "config" / "mcp_config.json"),
208
- ):
209
- reason = f"{name} is disabled in v0.1; no detection or configuration performed"
215
+ # Retain legacy init result slots without implicitly configuring hosts.
216
+ # Codex is supported only through the explicit install command; Antigravity
217
+ # remains unsupported. Clear stale activation claims in our own index only.
218
+ legacy_slots = (
219
+ (
220
+ "codex",
221
+ home / ".codex" / "config.toml",
222
+ "Codex is not configured by init; use install --host codex",
223
+ ),
224
+ (
225
+ "antigravity",
226
+ home / ".gemini" / "config" / "mcp_config.json",
227
+ "Antigravity is currently unsupported; no detection or configuration performed",
228
+ ),
229
+ )
230
+ for name, config_path, reason in legacy_slots:
210
231
  results[name] = result_from_detection(
211
232
  Detection(
212
233
  agent=name, detected=False, confidence="none", reason=reason,
@@ -911,6 +911,8 @@ class _MCPClient:
911
911
  stdout=subprocess.PIPE,
912
912
  stderr=subprocess.DEVNULL,
913
913
  text=True,
914
+ encoding="utf-8",
915
+ errors="strict",
914
916
  bufsize=1,
915
917
  )
916
918
  self._start_stdout_reader_locked(self._process)
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.1.7
2
+ version: 0.2.0
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -326,6 +326,10 @@ def _handle_codex_pre_tool(
326
326
  return {
327
327
  "hookSpecificOutput": {
328
328
  "hookEventName": "PreToolUse",
329
+ # Codex only applies updatedInput when the hook explicitly allows
330
+ # the call. Omitting this field makes the rewrite invalid and
331
+ # would drop memleaf's current-turn retrieval_id binding.
332
+ "permissionDecision": "allow",
329
333
  "updatedInput": prepared.arguments,
330
334
  }
331
335
  }
@@ -442,15 +442,26 @@ def install_codex(*, vault_path: Path | None = None) -> dict[str, Any]:
442
442
  "model": model,
443
443
  "codex": configured.to_dict(),
444
444
  }
445
+ model_ready = model.get("status") in {"configured", "already_configured"}
446
+ actions: list[str] = []
447
+ if configured.user_action_required and configured.user_action:
448
+ actions.append(configured.user_action)
449
+ if not model_ready:
450
+ actions.append(
451
+ "Configure an independent memleaf Model Route for this Vault before "
452
+ "relying on automatic memory extraction. Codex model/provider settings "
453
+ "are intentionally not used or modified."
454
+ )
445
455
  return {
446
456
  "status": configured.status,
447
457
  "reason": configured.reason,
448
458
  "vault": str(vault.root),
449
459
  "vault_source": vault_source,
450
460
  "model": model,
461
+ "processing_status": "ready" if model_ready else "model_route_required",
451
462
  "codex": configured.to_dict(),
452
- "user_action_required": configured.user_action_required,
453
- "user_action": configured.user_action,
463
+ "user_action_required": bool(actions),
464
+ "user_action": " ".join(actions) if actions else None,
454
465
  }
455
466
 
456
467
 
@@ -53,7 +53,7 @@ INSTRUCTIONS = (
53
53
  "user turn, including greetings, call memleaf search at least once before answering. Use your "
54
54
  "complete conversation context to select scope and query; do not add a needs-memory gate. "
55
55
  "Fetch further scope_catalog pages when the map has_more; never assume an omitted scope is absent. "
56
- "Search returns status=found or no_match and lightweight results (memory_id, title, scopes), "
56
+ "Search returns status=found or no_match and lightweight results (memory_id, title), "
57
57
  "not bodies. Errors are not no_match: correct scope_mismatch or retry a failed search at most "
58
58
  "twice, then honestly report degraded retrieval; do not claim memory was checked successfully. "
59
59
  "Treat directory entries as leads, never as verified facts; do not infer facts from a title. "
@@ -68,7 +68,7 @@ INSTRUCTIONS = (
68
68
  "scope/search/read flow. Managed MCP search is directory-only and rejects view=full; "
69
69
  "Python full-result search remains compatible. Without host integration, tools remain an explicit fallback only; "
70
70
  "do not claim automatic recall, recording or final-answer enforcement succeeded. "
71
- "Hermes is the only supported host in v0.1 and uses a Soft Gate, not a hard guarantee. "
71
+ "Hermes and Codex are supported hosts; Hermes uses a Soft Gate, not a hard guarantee. "
72
72
  "When explicitly calling legacy context, pass source and session_id together and avoid "
73
73
  "duplicating native memory that the target host already loads. "
74
74
  "Capture only user-visible and assistant-visible text. Never capture system or developer "
@@ -183,7 +183,7 @@ _TOOLS: tuple[dict[str, Any], ...] = (
183
183
  "name": "search",
184
184
  "description": (
185
185
  "Search once per ordinary user turn. Return found/no_match and paged lightweight "
186
- "results (memory_id, title, scopes), at most 20/4000 characters, without changing hits. "
186
+ "results (memory_id, title), at most 20/4000 characters, without changing hits. "
187
187
  "retrieval_id is required and must be the host-provided current-turn token. Errors are "
188
188
  "failures, never no_match. Managed MCP search is directory-only; view=full is rejected. "
189
189
  "Python search(view='full') remains the compatibility interface."
@@ -475,16 +475,18 @@ def _search_result(value: Any) -> dict[str, Any]:
475
475
  entries = value.get("results")
476
476
  if not isinstance(entries, list):
477
477
  raise ValueError("invalid search results")
478
+ results: list[dict[str, str]] = []
478
479
  for entry in entries:
479
480
  if not isinstance(entry, Mapping):
480
481
  raise ValueError("invalid search entry")
481
- if any(not isinstance(entry.get(name), str) or not entry[name] for name in ("memory_id", "title")):
482
+ memory_id = entry.get("memory_id")
483
+ title = entry.get("title")
484
+ if not isinstance(memory_id, str) or not memory_id or not isinstance(title, str) or not title:
482
485
  raise ValueError("invalid search identifiers")
483
- scopes = entry.get("scopes")
484
- if not isinstance(scopes, list) or not scopes or not all(isinstance(scope, str) and scope for scope in scopes):
485
- raise ValueError("invalid search scopes")
486
- results = [_directory_item(entry) for entry in entries]
487
- if any(entry is None for entry in results) or bool(results) != (value["status"] == "found"):
486
+ # Search is intentionally narrower than the legacy directory helper:
487
+ # do not leak per-memory scope/tags/aliases/keywords into candidates.
488
+ results.append({"memory_id": memory_id, "title": title})
489
+ if bool(results) != (value["status"] == "found"):
488
490
  raise ValueError("inconsistent search results")
489
491
  paging = _paging_fields(value)
490
492
  if not results and paging["has_more"]:
@@ -953,11 +955,24 @@ def _write_message(output: TextIO, message: Mapping[str, Any]) -> bool:
953
955
  try:
954
956
  output.write(_json_text(message) + "\n")
955
957
  output.flush()
956
- except (BrokenPipeError, OSError):
958
+ except (BrokenPipeError, OSError, UnicodeError):
957
959
  return False
958
960
  return True
959
961
 
960
962
 
963
+ def _configure_stdio_utf8() -> None:
964
+ """Force the stdio protocol channel to UTF-8 on every supported platform."""
965
+
966
+ for stream in (sys.stdout, sys.stderr):
967
+ reconfigure = getattr(stream, "reconfigure", None)
968
+ if not callable(reconfigure):
969
+ continue
970
+ try:
971
+ reconfigure(encoding="utf-8", errors="strict")
972
+ except (OSError, ValueError):
973
+ continue
974
+
975
+
961
976
  def serve(service: Memleaf, *, input_stream: Any = None, output_stream: TextIO = sys.stdout) -> int:
962
977
  """Serve newline-delimited JSON-RPC messages until EOF or a broken pipe."""
963
978
 
@@ -992,6 +1007,7 @@ def _parser() -> argparse.ArgumentParser:
992
1007
 
993
1008
 
994
1009
  def main(argv: list[str] | None = None) -> int:
1010
+ _configure_stdio_utf8()
995
1011
  args = _parser().parse_args(argv)
996
1012
  vault = args.vault if args.vault is not None else os.environ.get("MEMLEAF_VAULT") or None
997
1013
  try:
@@ -830,13 +830,13 @@ class Memleaf:
830
830
 
831
831
  @staticmethod
832
832
  def _candidate_directory(record: _Record) -> dict[str, Any]:
833
- # Keep the exact memory id and scope IDs usable by the next search;
834
- # only the human-facing title is shortened for a directory response.
833
+ # The finalized retrieval contract deliberately keeps candidate
834
+ # entries to memory_id + title. Scope selection already happened
835
+ # before search via the bounded Scope Map and search input.
835
836
  display = directory_entry(record.memory)
836
837
  return {
837
838
  "memory_id": record.memory.memory_id,
838
839
  "title": display.title,
839
- "scopes": list(record.memory.scopes),
840
840
  }
841
841
 
842
842
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.1.7
3
+ Version: 0.2.0
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.1.7。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.1.7 通过 PyPI 分发。
26
+ > **当前版本:0.2.0。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.0 通过 PyPI 分发。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
30
30
  ## 项目定位
@@ -65,7 +65,7 @@ memleaf 不会把整个 Vault 或整段历史对话自动塞进模型上下文
65
65
 
66
66
  1. Agent 使用当前完整会话和 Scope Map 选择检索范围与查询词;
67
67
  2. 至少调用一次 `search`;
68
- 3. `search` 只返回候选的 `memory_id`、标题和 Scope,不直接返回正文;
68
+ 3. `search` 候选默认只返回 `memory_id` 和标题;Scope 已由 Scope Map 与本次搜索参数确定,不随每条候选重复返回,也不直接返回正文;
69
69
  4. 只有需要引用事实时,才使用同一轮返回的 `retrieval_id` 调用 `read`;
70
70
  5. 根据读取到的关键记忆回答,不把所有候选全部读完。
71
71
 
@@ -172,6 +172,14 @@ python -m pip install -U memleaf && python -m memleaf install --host codex
172
172
 
173
173
  安装后打开 Codex,运行 `/hooks`,审核并信任 memleaf Hook。完成授权前,MCP 可用不等于自动捕获、检索门控和提炼已经激活;安装结果会明确显示 `pending_user_review`,不会把待审核状态报告为已启用。
174
174
 
175
+ Codex 只作为宿主,不作为 memleaf 的提炼模型来源。已有的独立 memleaf Model Route 会继续复用;如果当前 Vault 还没有完整 Model Route,Codex 安装仍可完成 MCP/Hook 配置,但结果会明确返回 `processing_status=model_route_required`,在配置模型前不能把“自动记忆提炼”视为已就绪。memleaf 不读取、不复制也不修改 Codex 的 `model`、`model_provider`、`base_url` 或认证信息,也不会为了提炼偷偷消耗 Codex 会话额度。Codex-only 用户可针对同一个 Vault 运行:
176
+
177
+ ```bash
178
+ python -m memleaf init --no-hermes --vault /path/to/the/same/vault
179
+ ```
180
+
181
+ 完成独立 Model Route 配置后,再使用 Codex 自动提炼。DeepSeek、OpenRouter 或其他自定义 Codex provider 因此保持原样。
182
+
175
183
  Antigravity(反重力)当前不支持,安装流程不会检测、安装或修改其配置。
176
184
 
177
185
  ### 高级初始化命令(可选)
@@ -63,8 +63,10 @@ src/memleaf/llm/router.py
63
63
  tests/__init__.py
64
64
  tests/test_admission_noise.py
65
65
  tests/test_codex_install.py
66
+ tests/test_codex_native_cli.py
66
67
  tests/test_context_budget.py
67
68
  tests/test_credential_safety.py
69
+ tests/test_cross_host_acceptance.py
68
70
  tests/test_hermes_provider.py
69
71
  tests/test_hermes_stdio_transport.py
70
72
  tests/test_host_events.py