memleaf 0.2.14__tar.gz → 0.2.16__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.14 → memleaf-0.2.16}/CHANGELOG.md +21 -0
- {memleaf-0.2.14/src/memleaf.egg-info → memleaf-0.2.16}/PKG-INFO +3 -3
- {memleaf-0.2.14 → memleaf-0.2.16}/README.en.md +2 -2
- {memleaf-0.2.14 → memleaf-0.2.16}/README.md +2 -2
- {memleaf-0.2.14 → memleaf-0.2.16}/pyproject.toml +1 -1
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/hermes_provider/__init__.py +60 -2
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/processing.py +422 -166
- {memleaf-0.2.14 → memleaf-0.2.16/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf.egg-info/SOURCES.txt +1 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_admission_noise.py +161 -28
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_hermes_provider.py +77 -6
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_pypi_install.py +5 -5
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b2a.py +13 -11
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_c3_packaging.py +1 -1
- memleaf-0.2.16/tests/test_update_target_recovery.py +896 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/LICENSE +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/MANIFEST.in +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/examples/README.md +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/examples/basic_usage.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/install.ps1 +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/install.sh +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/setup.cfg +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/config.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/index.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/models.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/service.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/validation.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/__init__.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_extraction_quality_regressions.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_host_events.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_install.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.14 → memleaf-0.2.16}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.16 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Keep read-only operational queries, derived overdue counters, and one-time
|
|
8
|
+
execution receipts out of permanent-memory extraction; preserve explicitly
|
|
9
|
+
confirmed durable facts and preferences.
|
|
10
|
+
- Deduplicate Hermes tool observations across cumulative conversation payloads,
|
|
11
|
+
preventing historical reads from being re-audited as uncontrolled while
|
|
12
|
+
retaining diagnostics for genuinely mismatched current-turn reads.
|
|
13
|
+
|
|
14
|
+
## 0.2.15 — 2026-09-02
|
|
15
|
+
|
|
16
|
+
- Recover a candidate whose `update_memory_id` has the wrong type at the
|
|
17
|
+
candidate level after bounded gate retries, without blocking unrelated
|
|
18
|
+
candidates or changing the active target's immutable type.
|
|
19
|
+
- Require candidate-local Scope membership and future-use relevance before
|
|
20
|
+
accepting a duplicate or update target, keeping cross-project targets
|
|
21
|
+
deferred and preventing unsafe overwrites.
|
|
22
|
+
- Keep duplicate validation independent from UPDATE type validation, and
|
|
23
|
+
safely defer unknown or same-use type-mismatch targets instead of creating
|
|
24
|
+
a sibling memory or failing the whole inbox turn.
|
|
25
|
+
|
|
5
26
|
## 0.2.14 — 2026-09-02
|
|
6
27
|
|
|
7
28
|
- Reject a permanent-memory candidate that combines a durable project rule or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.16
|
|
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.16。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.16 通过 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.16.**
|
|
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.16 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.16。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.16 通过 PyPI 分发。
|
|
9
9
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
10
10
|
|
|
11
11
|
## 项目定位
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Local-first Markdown memory core for AI agents."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.16"
|
|
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
|
|
@@ -49,6 +49,7 @@ _MAX_PENDING_TURN_NUMBERS = 128
|
|
|
49
49
|
_MAX_SESSION_ALIASES = 128
|
|
50
50
|
_MAX_LINEAGE_RETRIES = 2
|
|
51
51
|
_MAX_DEFERRED_PROCESS_SESSIONS = 128
|
|
52
|
+
_MAX_OBSERVED_TOOL_CALL_KEYS = 2048
|
|
52
53
|
_SAFE_COMPONENT_RE = re.compile(r"[^A-Za-z0-9_.-]+")
|
|
53
54
|
_PROVIDER_VERSION_RE = re.compile(r"^version:\s*([^\s#]+)\s*(?:#.*)?$", re.MULTILINE)
|
|
54
55
|
_ASCII_QUERY_TERM_RE = re.compile(r"[a-z0-9]+(?:[ ._-][a-z0-9]+)*")
|
|
@@ -628,6 +629,45 @@ def _tool_result_for_call(
|
|
|
628
629
|
return _CALL_FAILED
|
|
629
630
|
|
|
630
631
|
|
|
632
|
+
def _tool_observation_key(call: Mapping[str, Any], ordinal: int) -> str:
|
|
633
|
+
"""Build a stable in-memory identity for one visible tool call.
|
|
634
|
+
|
|
635
|
+
Hermes can pass the complete conversation back on every turn. The
|
|
636
|
+
retrieval token and result status are turn-relative, so they must not be
|
|
637
|
+
part of the identity: otherwise an old successful read is reclassified as
|
|
638
|
+
``uncontrolled_success`` when a newer token is observed. Call ids are
|
|
639
|
+
stable when Hermes supplies them; the ordinal/arguments fallback keeps
|
|
640
|
+
anonymous calls distinct within the cumulative message list.
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
arguments = call.get("arguments")
|
|
644
|
+
try:
|
|
645
|
+
arguments_text = json.dumps(arguments, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
646
|
+
except (TypeError, ValueError):
|
|
647
|
+
arguments_text = ""
|
|
648
|
+
call_id = call.get("call_id") if isinstance(call.get("call_id"), str) else ""
|
|
649
|
+
name = call.get("name") if isinstance(call.get("name"), str) else ""
|
|
650
|
+
if call_id:
|
|
651
|
+
identity = f"{name}\x00call-id\x00{call_id}"
|
|
652
|
+
else:
|
|
653
|
+
identity = f"{name}\x00anonymous\x00{ordinal}\x00{arguments_text}"
|
|
654
|
+
return sha256(identity.encode("utf-8")).hexdigest()
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _record_tool_observation(seen: Any, key: str) -> bool:
|
|
658
|
+
"""Record one observation key and report whether it was new."""
|
|
659
|
+
|
|
660
|
+
if seen is None:
|
|
661
|
+
return True
|
|
662
|
+
if key in seen:
|
|
663
|
+
return False
|
|
664
|
+
if isinstance(seen, set):
|
|
665
|
+
seen.add(key)
|
|
666
|
+
else:
|
|
667
|
+
seen[key] = None
|
|
668
|
+
return True
|
|
669
|
+
|
|
670
|
+
|
|
631
671
|
def _hermes_read_status(value: Any) -> str:
|
|
632
672
|
"""Classify a visible read result without inspecting it in diagnostics."""
|
|
633
673
|
|
|
@@ -1125,6 +1165,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1125
1165
|
self._gate_turn_ids: "OrderedDict[Tuple[str, int], str]" = OrderedDict()
|
|
1126
1166
|
self._active_turn_numbers: "OrderedDict[str, int]" = OrderedDict()
|
|
1127
1167
|
self._active_retrieval_ids: "OrderedDict[str, Optional[str]]" = OrderedDict()
|
|
1168
|
+
self._observed_tool_call_keys: "OrderedDict[str, None]" = OrderedDict()
|
|
1128
1169
|
self._version_warning_emitted = False
|
|
1129
1170
|
# Hermes keeps one provider instance alive while compression rotates
|
|
1130
1171
|
# the physical session id. Keep only a bounded alias chain so a
|
|
@@ -1752,6 +1793,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1752
1793
|
if not continuous:
|
|
1753
1794
|
self._pending_lineage.clear()
|
|
1754
1795
|
self._deferred_process_sessions.clear()
|
|
1796
|
+
self._observed_tool_call_keys.clear()
|
|
1755
1797
|
if _as_bool(reset, False) or _as_bool(rewound, False):
|
|
1756
1798
|
self._clear_session_turn_state(old_session_id or next_session_id)
|
|
1757
1799
|
for key in list(self._retrieval_ids_by_turn):
|
|
@@ -1826,6 +1868,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
1826
1868
|
self._gate_turn_ids.clear()
|
|
1827
1869
|
self._active_turn_numbers.clear()
|
|
1828
1870
|
self._active_retrieval_ids.clear()
|
|
1871
|
+
self._observed_tool_call_keys.clear()
|
|
1829
1872
|
self._session_aliases.clear()
|
|
1830
1873
|
self._pending_lineage.clear()
|
|
1831
1874
|
self._deferred_process_sessions.clear()
|
|
@@ -2073,6 +2116,7 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2073
2116
|
session_id: str = "",
|
|
2074
2117
|
turn_id: str = "",
|
|
2075
2118
|
vault_root: Optional[Path] = None,
|
|
2119
|
+
seen_call_keys: Any = None,
|
|
2076
2120
|
) -> str:
|
|
2077
2121
|
"""Observe explicit host MCP calls in public messages.
|
|
2078
2122
|
|
|
@@ -2087,25 +2131,32 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2087
2131
|
results = _visible_tool_results(messages)
|
|
2088
2132
|
statuses: list[str] = []
|
|
2089
2133
|
search_results_used: set[int] = set()
|
|
2134
|
+
search_ordinal = 0
|
|
2090
2135
|
for call in calls:
|
|
2091
2136
|
if call.get("name") != "mcp__memleaf__search":
|
|
2092
2137
|
continue
|
|
2138
|
+
search_ordinal += 1
|
|
2093
2139
|
arguments = call.get("arguments")
|
|
2094
2140
|
if not isinstance(arguments, Mapping) or arguments.get("retrieval_id") != retrieval_id:
|
|
2095
2141
|
continue
|
|
2096
2142
|
payload = _tool_result_for_call(call, calls, results, search_results_used)
|
|
2097
2143
|
if payload is not _CALL_FAILED:
|
|
2144
|
+
status = _hermes_search_status(payload)
|
|
2145
|
+
observation_key = _tool_observation_key(call, search_ordinal)
|
|
2146
|
+
if not _record_tool_observation(seen_call_keys, observation_key):
|
|
2147
|
+
continue
|
|
2098
2148
|
# Hermes has no public write-back hook for this soft observer.
|
|
2099
2149
|
# A valid current-turn result is enough to mark the local
|
|
2100
2150
|
# provider diagnostic; no Core ledger or body is touched here.
|
|
2101
|
-
statuses.append(
|
|
2151
|
+
statuses.append(status)
|
|
2102
2152
|
|
|
2103
2153
|
read_results_used: set[int] = set()
|
|
2104
2154
|
read_sequence = 0
|
|
2155
|
+
read_ordinal = 0
|
|
2105
2156
|
for call in calls:
|
|
2106
2157
|
if call.get("name") != "mcp__memleaf__read":
|
|
2107
2158
|
continue
|
|
2108
|
-
|
|
2159
|
+
read_ordinal += 1
|
|
2109
2160
|
arguments = call.get("arguments")
|
|
2110
2161
|
retrieval_present = isinstance(arguments, Mapping) and "retrieval_id" in arguments
|
|
2111
2162
|
retrieval_match = bool(
|
|
@@ -2120,6 +2171,10 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2120
2171
|
# mismatched gate binding. Do not make that look like a
|
|
2121
2172
|
# controlled success in the diagnostic stream.
|
|
2122
2173
|
result_status = "uncontrolled_success"
|
|
2174
|
+
observation_key = _tool_observation_key(call, read_ordinal)
|
|
2175
|
+
if not _record_tool_observation(seen_call_keys, observation_key):
|
|
2176
|
+
continue
|
|
2177
|
+
read_sequence += 1
|
|
2123
2178
|
logger.info(
|
|
2124
2179
|
"memleaf retrieval-read source=hermes session=%s turn=%s read_seq=%d retrieval_present=%s retrieval_match=%s result=%s",
|
|
2125
2180
|
_safe_component(session_id, "none") if session_id else "none",
|
|
@@ -2286,7 +2341,10 @@ class MemleafMemoryProvider(MemoryProvider):
|
|
|
2286
2341
|
session_id=effective_session,
|
|
2287
2342
|
turn_id=turn_id,
|
|
2288
2343
|
vault_root=_resolve_vault(self._config()),
|
|
2344
|
+
seen_call_keys=self._observed_tool_call_keys,
|
|
2289
2345
|
)
|
|
2346
|
+
while len(self._observed_tool_call_keys) > _MAX_OBSERVED_TOOL_CALL_KEYS:
|
|
2347
|
+
self._observed_tool_call_keys.popitem(last=False)
|
|
2290
2348
|
self._last_retrieval_observation = observation
|
|
2291
2349
|
lineage_ready = self._retry_pending_lineage(effective_session)
|
|
2292
2350
|
for role, content in visible_events:
|