memleaf 0.2.16__tar.gz → 0.2.18__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.16 → memleaf-0.2.18}/CHANGELOG.md +17 -0
- {memleaf-0.2.16/src/memleaf.egg-info → memleaf-0.2.18}/PKG-INFO +3 -3
- {memleaf-0.2.16 → memleaf-0.2.18}/README.en.md +2 -2
- {memleaf-0.2.16 → memleaf-0.2.18}/README.md +2 -2
- {memleaf-0.2.16 → memleaf-0.2.18}/install.sh +17 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/pyproject.toml +1 -1
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/hermes_provider/__init__.py +42 -6
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/installer.py +15 -0
- memleaf-0.2.18/src/memleaf/native_registration.py +123 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/processing.py +414 -47
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/prompts.py +22 -3
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/validation.py +13 -1
- {memleaf-0.2.16 → memleaf-0.2.18/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf.egg-info/SOURCES.txt +4 -0
- memleaf-0.2.18/tests/test_cross_turn_dedupe_regressions.py +195 -0
- memleaf-0.2.18/tests/test_hermes_native_registration.py +144 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_hermes_provider.py +91 -3
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_maintenance_v2.py +4 -2
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_pypi_install.py +5 -5
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_c3_packaging.py +1 -1
- memleaf-0.2.18/tests/test_todo_state_recovery.py +165 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/LICENSE +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/MANIFEST.in +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/examples/README.md +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/examples/basic_usage.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/install.ps1 +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/setup.cfg +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/config.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/index.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/models.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/service.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/__init__.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_host_events.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_install.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.16 → memleaf-0.2.18}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.18 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Register Hermes native `MEMORY.md` and `USER.md` as private, read-only native sources during supported install/upgrade flows while preserving custom native source configuration.
|
|
8
|
+
- Harden cross-turn deduplication and UPDATE/NO_CHANGE decisions: split independent future-use clauses, reuse one unambiguous same-scope active target, treat exact active duplicates as deterministic no-ops, and keep crash replay idempotent.
|
|
9
|
+
- Preserve the existing bounded Scope Map / metadata-directory retrieval flow without adding extra scope scans during automatic deduplication.
|
|
10
|
+
- Distinguish Hermes controlled read audit states: record `FOUND_READ` only after a current-token successful read, and use `FOUND_NO_READ_UNDETERMINED` when a FOUND result has no provable controlled read instead of overstating Soft Gate guarantees.
|
|
11
|
+
|
|
12
|
+
## 0.2.17 — 2026-09-02
|
|
13
|
+
|
|
14
|
+
- Preserve explicit user-confirmed completion or cancellation of one uniquely
|
|
15
|
+
related active todo as an in-place state update, even when the gate omits the
|
|
16
|
+
candidate or the same message asks what remains.
|
|
17
|
+
- Keep todo-state recovery deterministic and bounded: reuse the existing
|
|
18
|
+
memory ID, anchor `completed_at` to the user event, archive the old version,
|
|
19
|
+
and reject queries, negation, future intent, uncertainty, assistant-only
|
|
20
|
+
claims, and ambiguous targets.
|
|
21
|
+
|
|
5
22
|
## 0.2.16 — 2026-09-02
|
|
6
23
|
|
|
7
24
|
- Keep read-only operational queries, derived overdue counters, and one-time
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
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.18。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.18 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -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.18.**
|
|
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.18 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
|
|
@@ -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.18。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.18 通过 PyPI 分发。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
11
11
|
## 项目定位
|
|
@@ -347,6 +347,23 @@ PY
|
|
|
347
347
|
die "Hermes memleaf MCP configuration/test failed"
|
|
348
348
|
fi
|
|
349
349
|
|
|
350
|
+
# Register Hermes' own MEMORY.md and USER.md as private, read-only native
|
|
351
|
+
# sources only after both provider and MCP checks have succeeded. Missing
|
|
352
|
+
# files remain a valid indexed state and are never created by memleaf.
|
|
353
|
+
"$venv_path/bin/python" - "$vault_path" "$hermes_home" <<'PY'
|
|
354
|
+
from __future__ import annotations
|
|
355
|
+
|
|
356
|
+
import sys
|
|
357
|
+
from pathlib import Path
|
|
358
|
+
|
|
359
|
+
from memleaf.native_registration import ensure_hermes_native_sources
|
|
360
|
+
from memleaf.vault import Vault
|
|
361
|
+
|
|
362
|
+
vault = Vault(Path(sys.argv[1]).expanduser().resolve())
|
|
363
|
+
hermes_home = Path(sys.argv[2]).expanduser().resolve()
|
|
364
|
+
ensure_hermes_native_sources(vault, hermes_home)
|
|
365
|
+
PY
|
|
366
|
+
|
|
350
367
|
# The MCP status is written only after its own official test succeeds. This
|
|
351
368
|
# merge changes no Provider or other host fields.
|
|
352
369
|
"$venv_path/bin/python" - "$vault_path" <<'PY'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.18"
|
|
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
|
|
@@ -1181,6 +1181,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1181
1181
|
# under the original session id after the chain is restored.
|
|
1182
1182
|
self._deferred_process_sessions: "OrderedDict[str, None]" = OrderedDict()
|
|
1183
1183
|
self._last_retrieval_observation = "unknown"
|
|
1184
|
+
self._last_retrieval_audit = "SEARCH_UNKNOWN"
|
|
1184
1185
|
|
|
1185
1186
|
@property
|
|
1186
1187
|
def name(self) -> str:
|
|
@@ -1873,6 +1874,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1873
1874
|
self._pending_lineage.clear()
|
|
1874
1875
|
self._deferred_process_sessions.clear()
|
|
1875
1876
|
self._last_retrieval_observation = "unknown"
|
|
1877
|
+
self._last_retrieval_audit = "SEARCH_UNKNOWN"
|
|
1876
1878
|
self._version_warning_emitted = False
|
|
1877
1879
|
if self._client is not None:
|
|
1878
1880
|
self._client.close()
|
|
@@ -2117,6 +2119,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2117
2119
|
turn_id: str = "",
|
|
2118
2120
|
vault_root: Optional[Path] = None,
|
|
2119
2121
|
seen_call_keys: Any = None,
|
|
2122
|
+
audit_state: Optional[dict[str, Any]] = None,
|
|
2120
2123
|
) -> str:
|
|
2121
2124
|
"""Observe explicit host MCP calls in public messages.
|
|
2122
2125
|
|
|
@@ -2152,6 +2155,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2152
2155
|
|
|
2153
2156
|
read_results_used: set[int] = set()
|
|
2154
2157
|
read_sequence = 0
|
|
2158
|
+
controlled_reads = 0
|
|
2155
2159
|
read_ordinal = 0
|
|
2156
2160
|
for call in calls:
|
|
2157
2161
|
if call.get("name") != "mcp__memleaf__read":
|
|
@@ -2175,6 +2179,8 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2175
2179
|
if not _record_tool_observation(seen_call_keys, observation_key):
|
|
2176
2180
|
continue
|
|
2177
2181
|
read_sequence += 1
|
|
2182
|
+
if result_status == "ok" and retrieval_match:
|
|
2183
|
+
controlled_reads += 1
|
|
2178
2184
|
logger.info(
|
|
2179
2185
|
"memleaf retrieval-read source=hermes session=%s turn=%s read_seq=%d retrieval_present=%s retrieval_match=%s result=%s",
|
|
2180
2186
|
_safe_component(session_id, "none") if session_id else "none",
|
|
@@ -2202,12 +2208,39 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2202
2208
|
"detected" if bypass else "not_detected",
|
|
2203
2209
|
)
|
|
2204
2210
|
if not statuses:
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
+
search_status = "unknown"
|
|
2212
|
+
audit_status = "SEARCH_UNKNOWN"
|
|
2213
|
+
elif "found" in statuses:
|
|
2214
|
+
search_status = "found"
|
|
2215
|
+
# Hermes exposes no public pre-final hook and no reliable signal
|
|
2216
|
+
# that proves whether an answer used historical memory when no
|
|
2217
|
+
# controlled read occurred. Do not fabricate FOUND_NOT_USED or
|
|
2218
|
+
# FOUND_REQUIRED_READ_MISSING; record the uncertainty explicitly.
|
|
2219
|
+
audit_status = "FOUND_READ" if controlled_reads else "FOUND_NO_READ_UNDETERMINED"
|
|
2220
|
+
elif "no_match" in statuses:
|
|
2221
|
+
search_status = "no_match"
|
|
2222
|
+
audit_status = "NO_MATCH"
|
|
2223
|
+
else:
|
|
2224
|
+
search_status = "error"
|
|
2225
|
+
audit_status = "ERROR"
|
|
2226
|
+
if isinstance(audit_state, dict):
|
|
2227
|
+
audit_state.clear()
|
|
2228
|
+
audit_state.update(
|
|
2229
|
+
{
|
|
2230
|
+
"status": audit_status,
|
|
2231
|
+
"search_status": search_status,
|
|
2232
|
+
"controlled_reads": controlled_reads,
|
|
2233
|
+
}
|
|
2234
|
+
)
|
|
2235
|
+
logger.info(
|
|
2236
|
+
"memleaf retrieval-audit source=hermes session=%s turn=%s status=%s search=%s controlled_reads=%d",
|
|
2237
|
+
_safe_component(session_id, "none") if session_id else "none",
|
|
2238
|
+
_safe_component(turn_id, "none") if turn_id else "none",
|
|
2239
|
+
audit_status,
|
|
2240
|
+
search_status,
|
|
2241
|
+
controlled_reads,
|
|
2242
|
+
)
|
|
2243
|
+
return search_status
|
|
2211
2244
|
|
|
2212
2245
|
@staticmethod
|
|
2213
2246
|
def _catalog_retrieval_id(value: Any) -> Optional[str]:
|
|
@@ -2335,6 +2368,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2335
2368
|
if retrieval_id is None and resolved_turn_number is None:
|
|
2336
2369
|
retrieval_id = self._current_gate_id(effective_session)
|
|
2337
2370
|
if self._gate_enabled:
|
|
2371
|
+
audit_state: dict[str, Any] = {}
|
|
2338
2372
|
observation = self._observe_search_messages(
|
|
2339
2373
|
messages,
|
|
2340
2374
|
retrieval_id,
|
|
@@ -2342,10 +2376,12 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2342
2376
|
turn_id=turn_id,
|
|
2343
2377
|
vault_root=_resolve_vault(self._config()),
|
|
2344
2378
|
seen_call_keys=self._observed_tool_call_keys,
|
|
2379
|
+
audit_state=audit_state,
|
|
2345
2380
|
)
|
|
2346
2381
|
while len(self._observed_tool_call_keys) > _MAX_OBSERVED_TOOL_CALL_KEYS:
|
|
2347
2382
|
self._observed_tool_call_keys.popitem(last=False)
|
|
2348
2383
|
self._last_retrieval_observation = observation
|
|
2384
|
+
self._last_retrieval_audit = str(audit_state.get("status") or "SEARCH_UNKNOWN")
|
|
2349
2385
|
lineage_ready = self._retry_pending_lineage(effective_session)
|
|
2350
2386
|
for role, content in visible_events:
|
|
2351
2387
|
if not self._capture_visible(
|
|
@@ -22,6 +22,7 @@ from .adapters.codex import CodexAdapter
|
|
|
22
22
|
from .adapters.hermes import HermesAdapter, hermes_home_for_platform
|
|
23
23
|
from .cli import _home_from_environment, _prepare_model_route
|
|
24
24
|
from .locking import atomic_write_json
|
|
25
|
+
from .native_registration import ensure_hermes_native_sources
|
|
25
26
|
from .vault import Vault
|
|
26
27
|
|
|
27
28
|
|
|
@@ -415,6 +416,19 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
415
416
|
"model": model,
|
|
416
417
|
}
|
|
417
418
|
|
|
419
|
+
try:
|
|
420
|
+
native_registration = ensure_hermes_native_sources(vault, hermes_home)
|
|
421
|
+
except Exception:
|
|
422
|
+
return {
|
|
423
|
+
"status": "failure",
|
|
424
|
+
"reason": "Hermes native memory sources could not be registered safely",
|
|
425
|
+
"core_version": core_version,
|
|
426
|
+
"provider_version": provider_version,
|
|
427
|
+
"vault": str(vault.root),
|
|
428
|
+
"provider": str(provider_path),
|
|
429
|
+
"model": model,
|
|
430
|
+
}
|
|
431
|
+
|
|
418
432
|
update_agents_index(
|
|
419
433
|
vault.agents_index_path,
|
|
420
434
|
{
|
|
@@ -443,6 +457,7 @@ def install_hermes(*, vault_path: Path | None = None) -> dict[str, Any]:
|
|
|
443
457
|
"provider": str(provider_path),
|
|
444
458
|
"mcp_command": str(command),
|
|
445
459
|
"model": model,
|
|
460
|
+
"native_sources": native_registration,
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Host-native source registration helpers.
|
|
2
|
+
|
|
3
|
+
This module owns only configuration of read-only native sources. It never
|
|
4
|
+
creates, writes, renames, or removes a host's native memory files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Mapping
|
|
12
|
+
|
|
13
|
+
from .config import save_config
|
|
14
|
+
from .native_index import NativeIndexer, validate_native_sources
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_HERMES_SOURCES = (
|
|
18
|
+
("hermes_memory", "hermes_memory_builtin", "MEMORY.md"),
|
|
19
|
+
("hermes_user", "hermes_user_builtin", "USER.md"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _same_path(left: str | Path, right: str | Path) -> bool:
|
|
24
|
+
try:
|
|
25
|
+
return Path(left).expanduser().resolve(strict=False) == Path(right).expanduser().resolve(strict=False)
|
|
26
|
+
except (OSError, RuntimeError, ValueError):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _desired(path: Path) -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"agent": "hermes",
|
|
33
|
+
"path": str(path.expanduser().resolve(strict=False)),
|
|
34
|
+
"share": False,
|
|
35
|
+
"format": "markdown",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _safe_existing_for_hermes(source: Mapping[str, Any], path: Path) -> bool:
|
|
40
|
+
return (
|
|
41
|
+
source.get("agent") == "hermes"
|
|
42
|
+
and source.get("share") is False
|
|
43
|
+
and source.get("format", "markdown") == "markdown"
|
|
44
|
+
and _same_path(str(source.get("path", "")), path)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ensure_hermes_native_sources(vault: Any, hermes_home: Path | str) -> dict[str, Any]:
|
|
49
|
+
"""Idempotently register Hermes MEMORY.md and USER.md as private sources.
|
|
50
|
+
|
|
51
|
+
Existing user-defined native sources are preserved. A semantic conflict
|
|
52
|
+
fails closed instead of silently changing the user's source or exposing a
|
|
53
|
+
private Hermes file to another agent.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
home = Path(hermes_home).expanduser().resolve(strict=False)
|
|
57
|
+
targets = [(canonical, fallback, home / "memories" / filename) for canonical, fallback, filename in _HERMES_SOURCES]
|
|
58
|
+
|
|
59
|
+
with vault.lock():
|
|
60
|
+
config = vault.config()
|
|
61
|
+
existing_value = config.get("native_sources", {})
|
|
62
|
+
if not isinstance(existing_value, Mapping):
|
|
63
|
+
raise RuntimeError("invalid native_sources configuration")
|
|
64
|
+
sources: dict[str, dict[str, Any]] = {
|
|
65
|
+
str(source_id): deepcopy(dict(source))
|
|
66
|
+
for source_id, source in existing_value.items()
|
|
67
|
+
if isinstance(source_id, str) and isinstance(source, Mapping)
|
|
68
|
+
}
|
|
69
|
+
# Validate before touching anything. This also catches duplicate paths
|
|
70
|
+
# already present in a manually edited configuration.
|
|
71
|
+
validate_native_sources(sources, base_dir=vault.root)
|
|
72
|
+
changed = False
|
|
73
|
+
registered: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
for canonical, fallback, path in targets:
|
|
76
|
+
desired = _desired(path)
|
|
77
|
+
path_owner = next(
|
|
78
|
+
(
|
|
79
|
+
source_id
|
|
80
|
+
for source_id, source in sources.items()
|
|
81
|
+
if _same_path(str(source.get("path", "")), path)
|
|
82
|
+
),
|
|
83
|
+
None,
|
|
84
|
+
)
|
|
85
|
+
if path_owner is not None:
|
|
86
|
+
current = sources[path_owner]
|
|
87
|
+
if not _safe_existing_for_hermes(current, path):
|
|
88
|
+
raise RuntimeError(f"unsafe Hermes native source conflict: {path_owner}")
|
|
89
|
+
registered[canonical] = path_owner
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
target_id = canonical
|
|
93
|
+
if target_id in sources:
|
|
94
|
+
if _safe_existing_for_hermes(sources[target_id], path):
|
|
95
|
+
registered[canonical] = target_id
|
|
96
|
+
continue
|
|
97
|
+
target_id = fallback
|
|
98
|
+
if target_id in sources and not _safe_existing_for_hermes(sources[target_id], path):
|
|
99
|
+
raise RuntimeError(f"Hermes native source id conflict: {canonical}")
|
|
100
|
+
|
|
101
|
+
if target_id not in sources:
|
|
102
|
+
sources[target_id] = desired
|
|
103
|
+
changed = True
|
|
104
|
+
registered[canonical] = target_id
|
|
105
|
+
|
|
106
|
+
if changed:
|
|
107
|
+
updated = deepcopy(dict(config))
|
|
108
|
+
updated["native_sources"] = sources
|
|
109
|
+
save_config(vault.config_path, updated)
|
|
110
|
+
|
|
111
|
+
# Refresh the derived index under the same vault lock. Missing native
|
|
112
|
+
# files are a valid state and are represented as unavailable entries;
|
|
113
|
+
# this function never creates them.
|
|
114
|
+
index_summary = NativeIndexer(vault).refresh_unlocked()
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
"changed": changed,
|
|
118
|
+
"sources": registered,
|
|
119
|
+
"index": index_summary,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = ["ensure_hermes_native_sources"]
|