memleaf 0.2.9__tar.gz → 0.2.11__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.9 → memleaf-0.2.11}/CHANGELOG.md +27 -0
- {memleaf-0.2.9/src/memleaf.egg-info → memleaf-0.2.11}/PKG-INFO +10 -4
- {memleaf-0.2.9 → memleaf-0.2.11}/README.en.md +9 -3
- {memleaf-0.2.9 → memleaf-0.2.11}/README.md +9 -3
- {memleaf-0.2.9 → memleaf-0.2.11}/pyproject.toml +1 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/cli.py +18 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/hermes_provider/__init__.py +79 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/installer.py +43 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/processing.py +209 -41
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/prompts.py +6 -2
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/validation.py +287 -24
- {memleaf-0.2.9 → memleaf-0.2.11/src/memleaf.egg-info}/PKG-INFO +10 -4
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_hermes_provider.py +55 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_hermes_stdio_transport.py +3 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_pypi_install.py +71 -3
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b1.py +67 -26
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b2a.py +349 -14
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.9 → memleaf-0.2.11}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/LICENSE +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/MANIFEST.in +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/examples/README.md +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/examples/basic_usage.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/install.ps1 +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/install.sh +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/setup.cfg +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/config.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/index.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/models.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/service.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/__init__.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_host_events.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_install.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.11 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Document one complete macOS/Linux upgrade command that upgrades the core
|
|
8
|
+
package and refreshes the Hermes Provider together; report and verify both
|
|
9
|
+
versions after installation instead of silently accepting a stale copy.
|
|
10
|
+
- Compare the copied Provider manifest with the MCP server version at Hermes
|
|
11
|
+
startup and emit the same actionable one-line upgrade command when they
|
|
12
|
+
differ or cannot be verified.
|
|
13
|
+
- Normalize supported one-off Chinese and English relative dates
|
|
14
|
+
deterministically from their current evidence timestamps before strict
|
|
15
|
+
summary validation, including conflicting parenthesized numeric dates.
|
|
16
|
+
- Keep ambiguous or unsupported relative-date candidates retryable without
|
|
17
|
+
guessing, while allowing valid sibling candidates from the same turn to
|
|
18
|
+
commit and preserving the source inbox.
|
|
19
|
+
|
|
20
|
+
## 0.2.10 — 2026-09-01
|
|
21
|
+
|
|
22
|
+
- Keep strict candidate-local project Scope grounding for the first two gate
|
|
23
|
+
attempts, then isolate a persistently invalid candidate instead of failing
|
|
24
|
+
the entire mailbox turn.
|
|
25
|
+
- Correct a final-attempt model Scope only when the candidate text identifies
|
|
26
|
+
exactly one registered project name or alias; defer zero-match and
|
|
27
|
+
multi-project candidates as insufficient context without writing memory.
|
|
28
|
+
- Preserve valid sibling candidates, new explicitly named project Scopes, and
|
|
29
|
+
the existing update-target relevance guard when Scope and target errors occur
|
|
30
|
+
together.
|
|
31
|
+
|
|
5
32
|
## 0.2.9 — 2026-09-01
|
|
6
33
|
|
|
7
34
|
- Reject update and duplicate targets that are relevant only to another item
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.11
|
|
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.11。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.11 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -132,7 +132,13 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
132
132
|
|
|
133
133
|
### 更新 memleaf
|
|
134
134
|
|
|
135
|
-
安装命令同时也是升级命令,不需要先卸载旧版本。Windows 用户重新执行上面的 PowerShell 一行命令;macOS / Linux
|
|
135
|
+
安装命令同时也是升级命令,不需要先卸载旧版本。Windows 用户重新执行上面的 PowerShell 一行命令;macOS / Linux 用户升级时请重新执行下面这一行完整命令,它会同时升级核心包并刷新 Hermes Provider:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python -m pip install -U memleaf && python -m memleaf install
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
不要只执行 `pip install -U memleaf`;那只会升级核心包,不会刷新 `~/.hermes/plugins/memleaf` 中的 Provider。
|
|
136
142
|
|
|
137
143
|
升级时不会迁移或删除现有记忆。Vault 选择顺序固定为:
|
|
138
144
|
|
|
@@ -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.11.**
|
|
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.11 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
|
|
@@ -113,7 +113,13 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
113
113
|
|
|
114
114
|
### Updating memleaf
|
|
115
115
|
|
|
116
|
-
The install command is also the upgrade command; an old version does not need to be uninstalled first. Windows users can rerun the PowerShell one-liner above
|
|
116
|
+
The install command is also the upgrade command; an old version does not need to be uninstalled first. Windows users can rerun the PowerShell one-liner above. On macOS / Linux, run this complete one-line command to upgrade the core package and refresh the Hermes Provider together:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python -m pip install -U memleaf && python -m memleaf install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Do not run only `pip install -U memleaf`; that upgrades the core package but does not refresh the Provider under `~/.hermes/plugins/memleaf`.
|
|
117
123
|
|
|
118
124
|
Upgrades do not migrate or delete existing memories. Vault selection has a fixed precedence:
|
|
119
125
|
|
|
@@ -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.11。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.11 通过 PyPI 分发。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
11
11
|
## 项目定位
|
|
@@ -113,7 +113,13 @@ python -m pip install -U memleaf && python -m memleaf install
|
|
|
113
113
|
|
|
114
114
|
### 更新 memleaf
|
|
115
115
|
|
|
116
|
-
安装命令同时也是升级命令,不需要先卸载旧版本。Windows 用户重新执行上面的 PowerShell 一行命令;macOS / Linux
|
|
116
|
+
安装命令同时也是升级命令,不需要先卸载旧版本。Windows 用户重新执行上面的 PowerShell 一行命令;macOS / Linux 用户升级时请重新执行下面这一行完整命令,它会同时升级核心包并刷新 Hermes Provider:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python -m pip install -U memleaf && python -m memleaf install
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
不要只执行 `pip install -U memleaf`;那只会升级核心包,不会刷新 `~/.hermes/plugins/memleaf` 中的 Provider。
|
|
117
123
|
|
|
118
124
|
升级时不会迁移或删除现有记忆。Vault 选择顺序固定为:
|
|
119
125
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.11"
|
|
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
|
|
@@ -127,6 +127,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
127
127
|
print(json.dumps(output, ensure_ascii=False, sort_keys=True))
|
|
128
128
|
elif output.get("status") in {"configured", "already_configured"}:
|
|
129
129
|
print(f"memleaf installed for {args.host}: {output['vault']}")
|
|
130
|
+
if args.host == "hermes" and (
|
|
131
|
+
output.get("core_version") is not None
|
|
132
|
+
or output.get("provider_version") is not None
|
|
133
|
+
):
|
|
134
|
+
print(
|
|
135
|
+
"memleaf versions: "
|
|
136
|
+
f"core={output.get('core_version') or 'unknown'}, "
|
|
137
|
+
f"Hermes provider={output.get('provider_version') or 'unknown'}"
|
|
138
|
+
)
|
|
130
139
|
if output.get("vault_source") == "hermes_config":
|
|
131
140
|
print("Preserved the Vault from the existing Hermes memleaf configuration.")
|
|
132
141
|
if args.host == "hermes":
|
|
@@ -136,6 +145,15 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
136
145
|
if output.get("model", {}).get("status") == "not_configured":
|
|
137
146
|
print("Configure a memleaf model route before using automatic processing.")
|
|
138
147
|
else:
|
|
148
|
+
if args.host == "hermes" and (
|
|
149
|
+
output.get("core_version") is not None
|
|
150
|
+
or output.get("provider_version") is not None
|
|
151
|
+
):
|
|
152
|
+
print(
|
|
153
|
+
"memleaf versions: "
|
|
154
|
+
f"core={output.get('core_version') or 'unknown'}, "
|
|
155
|
+
f"Hermes provider={output.get('provider_version') or 'unknown'}"
|
|
156
|
+
)
|
|
139
157
|
print(f"memleaf install failed: {output.get('reason', 'unknown error')}", file=sys.stderr)
|
|
140
158
|
return 0 if output.get("status") in {"configured", "already_configured"} else 2
|
|
141
159
|
if args.json:
|
|
@@ -32,6 +32,7 @@ _DEFAULT_TIMEOUT = 5.0
|
|
|
32
32
|
_MAX_TIMEOUT = 30.0
|
|
33
33
|
_DEFAULT_PROCESS_TIMEOUT = 300.0
|
|
34
34
|
_MAX_PROCESS_TIMEOUT = 900.0
|
|
35
|
+
_UPDATE_COMMAND = "python -m pip install -U memleaf && python -m memleaf install"
|
|
35
36
|
_MAX_SCOPE_ITEMS = 20
|
|
36
37
|
_MAX_SCOPE_CHARS = 2000
|
|
37
38
|
_SCOPE_MAP_INCOMPLETE = (
|
|
@@ -49,6 +50,7 @@ _MAX_SESSION_ALIASES = 128
|
|
|
49
50
|
_MAX_LINEAGE_RETRIES = 2
|
|
50
51
|
_MAX_DEFERRED_PROCESS_SESSIONS = 128
|
|
51
52
|
_SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9_.-]+")
|
|
53
|
+
_PROVIDER_VERSION_RE = re.compile(r"^version:\s*([^\s#]+)\s*(?:#.*)?$", re.MULTILINE)
|
|
52
54
|
_ASCII_QUERY_TERM_RE = re.compile(r"[a-z0-9]+(?:[ ._-][a-z0-9]+)*")
|
|
53
55
|
_DISABLED_PLATFORMS = frozenset({"cron"})
|
|
54
56
|
_DISABLED_AGENT_CONTEXTS = frozenset({"cron", "flush", "subagent"})
|
|
@@ -704,6 +706,28 @@ def _config_path(hermes_home: str | Path) -> Path:
|
|
|
704
706
|
return Path(hermes_home).expanduser() / "memleaf.json"
|
|
705
707
|
|
|
706
708
|
|
|
709
|
+
def _version_value(value: Any) -> Optional[str]:
|
|
710
|
+
"""Return a bounded, log-safe version string or ``None``."""
|
|
711
|
+
|
|
712
|
+
if not isinstance(value, str):
|
|
713
|
+
return None
|
|
714
|
+
value = value.strip()
|
|
715
|
+
if not value or len(value) > 64 or any(char.isspace() for char in value):
|
|
716
|
+
return None
|
|
717
|
+
return value
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
def _provider_manifest_version() -> Optional[str]:
|
|
721
|
+
"""Read the version from this copied provider's adjacent manifest."""
|
|
722
|
+
|
|
723
|
+
try:
|
|
724
|
+
text = Path(__file__).with_name("plugin.yaml").read_text(encoding="utf-8")
|
|
725
|
+
except (OSError, UnicodeError, RuntimeError):
|
|
726
|
+
return None
|
|
727
|
+
match = _PROVIDER_VERSION_RE.search(text)
|
|
728
|
+
return _version_value(match.group(1)) if match else None
|
|
729
|
+
|
|
730
|
+
|
|
707
731
|
def _default_hermes_home() -> Path:
|
|
708
732
|
configured = os.environ.get("HERMES_HOME")
|
|
709
733
|
return Path(configured).expanduser() if configured else Path.home() / ".hermes"
|
|
@@ -863,6 +887,7 @@ class _MCPClient:
|
|
|
863
887
|
self._stdout_thread: Optional[threading.Thread] = None
|
|
864
888
|
self._next_id = 1
|
|
865
889
|
self._lock = threading.RLock()
|
|
890
|
+
self.server_version: Optional[str] = None
|
|
866
891
|
|
|
867
892
|
def _resolve_command(self) -> str:
|
|
868
893
|
path = Path(self.command).expanduser()
|
|
@@ -921,7 +946,7 @@ class _MCPClient:
|
|
|
921
946
|
bufsize=1,
|
|
922
947
|
)
|
|
923
948
|
self._start_stdout_reader_locked(self._process)
|
|
924
|
-
self._request_locked(
|
|
949
|
+
initialize_result = self._request_locked(
|
|
925
950
|
"initialize",
|
|
926
951
|
{
|
|
927
952
|
"protocolVersion": "2024-11-05",
|
|
@@ -929,6 +954,13 @@ class _MCPClient:
|
|
|
929
954
|
"clientInfo": {"name": "hermes-memleaf", "version": "0.1.0"},
|
|
930
955
|
},
|
|
931
956
|
)
|
|
957
|
+
server_info = (
|
|
958
|
+
initialize_result.get("serverInfo")
|
|
959
|
+
if isinstance(initialize_result, Mapping)
|
|
960
|
+
else None
|
|
961
|
+
)
|
|
962
|
+
if isinstance(server_info, Mapping):
|
|
963
|
+
self.server_version = _version_value(server_info.get("version"))
|
|
932
964
|
self._send_locked({"jsonrpc": "2.0", "method": "notifications/initialized"})
|
|
933
965
|
|
|
934
966
|
def _send_locked(self, message: Mapping[str, Any]) -> None:
|
|
@@ -983,6 +1015,7 @@ class _MCPClient:
|
|
|
983
1015
|
self._process = None
|
|
984
1016
|
self._stdout_queue = None
|
|
985
1017
|
self._stdout_thread = None
|
|
1018
|
+
self.server_version = None
|
|
986
1019
|
if process is None:
|
|
987
1020
|
return
|
|
988
1021
|
if process.stdin is not None:
|
|
@@ -1091,6 +1124,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1091
1124
|
self._gate_turn_ids: "OrderedDict[Tuple[str, int], str]" = OrderedDict()
|
|
1092
1125
|
self._active_turn_numbers: "OrderedDict[str, int]" = OrderedDict()
|
|
1093
1126
|
self._active_retrieval_ids: "OrderedDict[str, Optional[str]]" = OrderedDict()
|
|
1127
|
+
self._version_warning_emitted = False
|
|
1094
1128
|
# Hermes keeps one provider instance alive while compression rotates
|
|
1095
1129
|
# the physical session id. Keep only a bounded alias chain so a
|
|
1096
1130
|
# callback already queued for the parent can still resolve to the
|
|
@@ -1506,6 +1540,48 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1506
1540
|
_safe_component(turn_id, "none") if turn_id else "none",
|
|
1507
1541
|
)
|
|
1508
1542
|
|
|
1543
|
+
def _check_version_sync(self) -> None:
|
|
1544
|
+
"""Warn when the copied provider and MCP core came from different releases."""
|
|
1545
|
+
|
|
1546
|
+
client = self._client
|
|
1547
|
+
if client is None:
|
|
1548
|
+
return
|
|
1549
|
+
# Test doubles and older host integrations may not expose the MCP
|
|
1550
|
+
# initialize metadata. The real client always does, so only suppress
|
|
1551
|
+
# this check for a double that has no version attribute at all.
|
|
1552
|
+
client_type = _MCPClient
|
|
1553
|
+
is_real_client = isinstance(client_type, type) and isinstance(client, client_type)
|
|
1554
|
+
client_fields = getattr(client, "__dict__", {})
|
|
1555
|
+
if not is_real_client and not (
|
|
1556
|
+
isinstance(client_fields, Mapping) and "server_version" in client_fields
|
|
1557
|
+
):
|
|
1558
|
+
return
|
|
1559
|
+
provider_version = _provider_manifest_version()
|
|
1560
|
+
core_version = _version_value(getattr(client, "server_version", None))
|
|
1561
|
+
if provider_version is None or core_version is None:
|
|
1562
|
+
if not self._version_warning_emitted:
|
|
1563
|
+
logger.warning(
|
|
1564
|
+
"memleaf provider/core version check unavailable "
|
|
1565
|
+
"(provider=%s core=%s); do not assume they are synchronized. "
|
|
1566
|
+
"Run: %s",
|
|
1567
|
+
provider_version or "unknown",
|
|
1568
|
+
core_version or "unknown",
|
|
1569
|
+
_UPDATE_COMMAND,
|
|
1570
|
+
)
|
|
1571
|
+
self._version_warning_emitted = True
|
|
1572
|
+
return
|
|
1573
|
+
if provider_version == core_version:
|
|
1574
|
+
return
|
|
1575
|
+
if not self._version_warning_emitted:
|
|
1576
|
+
logger.warning(
|
|
1577
|
+
"memleaf provider/core version mismatch (provider=%s core=%s). "
|
|
1578
|
+
"Run: %s",
|
|
1579
|
+
provider_version,
|
|
1580
|
+
core_version,
|
|
1581
|
+
_UPDATE_COMMAND,
|
|
1582
|
+
)
|
|
1583
|
+
self._version_warning_emitted = True
|
|
1584
|
+
|
|
1509
1585
|
def _queue_turn_number(self, turn_number: Any, message: Any) -> None:
|
|
1510
1586
|
if isinstance(turn_number, bool) or not isinstance(turn_number, int) or turn_number <= 0:
|
|
1511
1587
|
return
|
|
@@ -1753,6 +1829,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1753
1829
|
self._pending_lineage.clear()
|
|
1754
1830
|
self._deferred_process_sessions.clear()
|
|
1755
1831
|
self._last_retrieval_observation = "unknown"
|
|
1832
|
+
self._version_warning_emitted = False
|
|
1756
1833
|
if self._client is not None:
|
|
1757
1834
|
self._client.close()
|
|
1758
1835
|
self._client = None
|
|
@@ -1795,6 +1872,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1795
1872
|
session_id=self._session_id,
|
|
1796
1873
|
)
|
|
1797
1874
|
self._call("stats", {}, stage="stats", session_id=self._session_id)
|
|
1875
|
+
self._check_version_sync()
|
|
1798
1876
|
|
|
1799
1877
|
def system_prompt_block(self) -> str:
|
|
1800
1878
|
if not self._write_enabled:
|
|
@@ -6,6 +6,7 @@ import json
|
|
|
6
6
|
import os
|
|
7
7
|
from importlib import resources
|
|
8
8
|
from pathlib import Path
|
|
9
|
+
import re
|
|
9
10
|
import shutil
|
|
10
11
|
import site
|
|
11
12
|
import stat
|
|
@@ -15,6 +16,7 @@ import sysconfig
|
|
|
15
16
|
import tempfile
|
|
16
17
|
from typing import Any
|
|
17
18
|
|
|
19
|
+
from . import __version__
|
|
18
20
|
from .adapters.base import update_agents_index
|
|
19
21
|
from .adapters.codex import CodexAdapter
|
|
20
22
|
from .adapters.hermes import HermesAdapter, hermes_home_for_platform
|
|
@@ -24,6 +26,7 @@ from .vault import Vault
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
_EXPECTED_TOOLS = 11
|
|
29
|
+
_PROVIDER_VERSION_RE = re.compile(r"^version:\s*([^\s#]+)\s*(?:#.*)?$", re.MULTILINE)
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
def _hermes_home(
|
|
@@ -232,6 +235,17 @@ def _copy_provider(hermes_home: Path) -> Path:
|
|
|
232
235
|
return target
|
|
233
236
|
|
|
234
237
|
|
|
238
|
+
def _provider_manifest_version(provider_path: Path) -> str | None:
|
|
239
|
+
"""Read the copied Hermes provider version without requiring a YAML parser."""
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
text = (provider_path / "plugin.yaml").read_text(encoding="utf-8")
|
|
243
|
+
except (OSError, UnicodeError):
|
|
244
|
+
return None
|
|
245
|
+
match = _PROVIDER_VERSION_RE.search(text)
|
|
246
|
+
return match.group(1) if match else None
|
|
247
|
+
|
|
248
|
+
|
|
235
249
|
def _write_provider_config(path: Path, command: Path, vault: Path) -> None:
|
|
236
250
|
if path.is_symlink():
|
|
237
251
|
raise RuntimeError(f"refusing to write symlinked Hermes config: {path}")
|
|
@@ -289,6 +303,7 @@ def _verify_provider(hermes: str) -> bool:
|
|
|
289
303
|
def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
290
304
|
"""Install and configure memleaf for Hermes from the PyPI package."""
|
|
291
305
|
|
|
306
|
+
core_version = __version__
|
|
292
307
|
home = _home_from_environment()
|
|
293
308
|
hermes_home = _hermes_home(home)
|
|
294
309
|
selected_vault, vault_source = _select_vault_path(
|
|
@@ -308,6 +323,8 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
308
323
|
return {
|
|
309
324
|
"status": "failure",
|
|
310
325
|
"reason": "model route is not configured",
|
|
326
|
+
"core_version": core_version,
|
|
327
|
+
"provider_version": None,
|
|
311
328
|
"vault": str(vault.root),
|
|
312
329
|
"model": model,
|
|
313
330
|
}
|
|
@@ -318,12 +335,28 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
318
335
|
return {
|
|
319
336
|
"status": "failure",
|
|
320
337
|
"reason": "Hermes executable was not found",
|
|
338
|
+
"core_version": core_version,
|
|
339
|
+
"provider_version": None,
|
|
321
340
|
"vault": str(vault.root),
|
|
322
341
|
"model": model,
|
|
323
342
|
}
|
|
324
343
|
|
|
325
344
|
command = _memleaf_mcp_command()
|
|
326
345
|
provider_path = _copy_provider(hermes_home)
|
|
346
|
+
provider_version = _provider_manifest_version(provider_path)
|
|
347
|
+
if provider_version != core_version:
|
|
348
|
+
return {
|
|
349
|
+
"status": "failure",
|
|
350
|
+
"reason": (
|
|
351
|
+
"Hermes provider version mismatch after copy: "
|
|
352
|
+
f"core={core_version}, provider={provider_version or 'unknown'}"
|
|
353
|
+
),
|
|
354
|
+
"core_version": core_version,
|
|
355
|
+
"provider_version": provider_version,
|
|
356
|
+
"vault": str(vault.root),
|
|
357
|
+
"provider": str(provider_path),
|
|
358
|
+
"model": model,
|
|
359
|
+
}
|
|
327
360
|
_write_provider_config(hermes_home / "memleaf.json", command, vault.root)
|
|
328
361
|
|
|
329
362
|
activated = _run([detection.executable, "config", "set", "memory.provider", "memleaf"])
|
|
@@ -331,6 +364,8 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
331
364
|
return {
|
|
332
365
|
"status": "failure",
|
|
333
366
|
"reason": "Hermes MemoryProvider could not be activated",
|
|
367
|
+
"core_version": core_version,
|
|
368
|
+
"provider_version": provider_version,
|
|
334
369
|
"vault": str(vault.root),
|
|
335
370
|
"provider": str(provider_path),
|
|
336
371
|
"model": model,
|
|
@@ -348,6 +383,8 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
348
383
|
return {
|
|
349
384
|
"status": "failure",
|
|
350
385
|
"reason": "Hermes MCP entry could not be configured",
|
|
386
|
+
"core_version": core_version,
|
|
387
|
+
"provider_version": provider_version,
|
|
351
388
|
"vault": str(vault.root),
|
|
352
389
|
"provider": str(provider_path),
|
|
353
390
|
"model": model,
|
|
@@ -357,6 +394,8 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
357
394
|
return {
|
|
358
395
|
"status": "failure",
|
|
359
396
|
"reason": "Hermes MCP lifecycle could not be configured",
|
|
397
|
+
"core_version": core_version,
|
|
398
|
+
"provider_version": provider_version,
|
|
360
399
|
"vault": str(vault.root),
|
|
361
400
|
"provider": str(provider_path),
|
|
362
401
|
"model": model,
|
|
@@ -369,6 +408,8 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
369
408
|
return {
|
|
370
409
|
"status": "failure",
|
|
371
410
|
"reason": "Hermes MCP test did not confirm 11 tools",
|
|
411
|
+
"core_version": core_version,
|
|
412
|
+
"provider_version": provider_version,
|
|
372
413
|
"vault": str(vault.root),
|
|
373
414
|
"provider": str(provider_path),
|
|
374
415
|
"model": model,
|
|
@@ -395,6 +436,8 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
395
436
|
return {
|
|
396
437
|
"status": "configured",
|
|
397
438
|
"reason": "memleaf is fully configured for Hermes",
|
|
439
|
+
"core_version": core_version,
|
|
440
|
+
"provider_version": provider_version,
|
|
398
441
|
"vault": str(vault.root),
|
|
399
442
|
"vault_source": vault_source,
|
|
400
443
|
"provider": str(provider_path),
|