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.
Files changed (102) hide show
  1. {memleaf-0.2.9 → memleaf-0.2.11}/CHANGELOG.md +27 -0
  2. {memleaf-0.2.9/src/memleaf.egg-info → memleaf-0.2.11}/PKG-INFO +10 -4
  3. {memleaf-0.2.9 → memleaf-0.2.11}/README.en.md +9 -3
  4. {memleaf-0.2.9 → memleaf-0.2.11}/README.md +9 -3
  5. {memleaf-0.2.9 → memleaf-0.2.11}/pyproject.toml +1 -1
  6. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/cli.py +18 -0
  8. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/hermes_provider/__init__.py +79 -1
  9. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  10. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/installer.py +43 -0
  11. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/processing.py +209 -41
  12. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/prompts.py +6 -2
  13. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/validation.py +287 -24
  14. {memleaf-0.2.9 → memleaf-0.2.11/src/memleaf.egg-info}/PKG-INFO +10 -4
  15. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_hermes_provider.py +55 -0
  16. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_hermes_stdio_transport.py +3 -1
  17. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_pypi_install.py +71 -3
  18. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b1.py +67 -26
  19. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b2a.py +349 -14
  20. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_c1_mcp.py +1 -1
  21. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_c3_packaging.py +1 -1
  22. {memleaf-0.2.9 → memleaf-0.2.11}/IMPLEMENTATION_PLAN.md +0 -0
  23. {memleaf-0.2.9 → memleaf-0.2.11}/LICENSE +0 -0
  24. {memleaf-0.2.9 → memleaf-0.2.11}/MANIFEST.in +0 -0
  25. {memleaf-0.2.9 → memleaf-0.2.11}/RELEASE_CHECKLIST.md +0 -0
  26. {memleaf-0.2.9 → memleaf-0.2.11}/examples/README.md +0 -0
  27. {memleaf-0.2.9 → memleaf-0.2.11}/examples/basic_usage.py +0 -0
  28. {memleaf-0.2.9 → memleaf-0.2.11}/examples/mcp_stdio.ndjson +0 -0
  29. {memleaf-0.2.9 → memleaf-0.2.11}/install.ps1 +0 -0
  30. {memleaf-0.2.9 → memleaf-0.2.11}/install.sh +0 -0
  31. {memleaf-0.2.9 → memleaf-0.2.11}/setup.cfg +0 -0
  32. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/__main__.py +0 -0
  33. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/__init__.py +0 -0
  34. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/antigravity.py +0 -0
  35. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/base.py +0 -0
  36. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/codex.py +0 -0
  37. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/adapters/hermes.py +0 -0
  38. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/budget.py +0 -0
  39. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/capture.py +0 -0
  40. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/compaction.py +0 -0
  41. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/config.py +0 -0
  42. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/credentials.py +0 -0
  43. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/frontmatter.py +0 -0
  44. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/hermes_provider/README.md +0 -0
  45. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/host_events.py +0 -0
  46. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/host_runtime.py +0 -0
  47. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/inbox.py +0 -0
  48. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/index.py +0 -0
  49. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/__init__.py +0 -0
  50. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/base.py +0 -0
  51. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/claude_compatible.py +0 -0
  52. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/gemini.py +0 -0
  53. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/openai_compatible.py +0 -0
  54. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/llm/router.py +0 -0
  55. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/locking.py +0 -0
  56. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/mcp_server.py +0 -0
  57. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/memory_writer.py +0 -0
  58. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/model_discovery.py +0 -0
  59. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/models.py +0 -0
  60. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/native_index.py +0 -0
  61. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/redaction.py +0 -0
  62. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/retrieval.py +0 -0
  63. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/retrieval_gate.py +0 -0
  64. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/scope_maintenance.py +0 -0
  65. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/scope_state.py +0 -0
  66. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/service.py +0 -0
  67. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf/vault.py +0 -0
  68. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/SOURCES.txt +0 -0
  69. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/dependency_links.txt +0 -0
  70. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/entry_points.txt +0 -0
  71. {memleaf-0.2.9 → memleaf-0.2.11}/src/memleaf.egg-info/top_level.txt +0 -0
  72. {memleaf-0.2.9 → memleaf-0.2.11}/tests/__init__.py +0 -0
  73. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_admission_noise.py +0 -0
  74. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_codex_install.py +0 -0
  75. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_codex_native_cli.py +0 -0
  76. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_context_budget.py +0 -0
  77. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_credential_safety.py +0 -0
  78. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_cross_host_acceptance.py +0 -0
  79. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_host_events.py +0 -0
  80. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_host_runtime_contract.py +0 -0
  81. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_install.py +0 -0
  82. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_maintenance_v2.py +0 -0
  83. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_model_discovery.py +0 -0
  84. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_retrieval_gate.py +0 -0
  85. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_retrieval_v2.py +0 -0
  86. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_session_lineage.py +0 -0
  87. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_a.py +0 -0
  88. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b2b.py +0 -0
  89. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3a_commit.py +0 -0
  90. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3a_contract.py +0 -0
  91. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3b_native_context.py +0 -0
  92. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3b_native_index.py +0 -0
  93. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3b_scope.py +0 -0
  94. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3c_retrieval.py +0 -0
  95. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  96. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_stage_c2_init.py +0 -0
  97. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_upgrade_preserves_vault.py +0 -0
  98. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_gate_limits.py +0 -0
  99. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_host_flow.py +0 -0
  100. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_mcp_flow.py +0 -0
  101. {memleaf-0.2.9 → memleaf-0.2.11}/tests/test_v2_nomatch_semantics.py +0 -0
  102. {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.9
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.9。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.9 通过 PyPI 分发。
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 用户重新执行上面的 `pip install -U` 命令即可。
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.9.**
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.9 is distributed through PyPI.
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, while macOS / Linux users can rerun the `pip install -U` command.
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.9。**
8
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.9 通过 PyPI 分发。
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 用户重新执行上面的 `pip install -U` 命令即可。
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.9"
7
+ version = "0.2.11"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.9"
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:
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.9
2
+ version: 0.2.11
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -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),