memleaf 0.2.18__tar.gz → 0.2.19__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.18 → memleaf-0.2.19}/CHANGELOG.md +9 -0
- {memleaf-0.2.18/src/memleaf.egg-info → memleaf-0.2.19}/PKG-INFO +3 -3
- {memleaf-0.2.18 → memleaf-0.2.19}/README.en.md +2 -2
- {memleaf-0.2.18 → memleaf-0.2.19}/README.md +2 -2
- {memleaf-0.2.18 → memleaf-0.2.19}/pyproject.toml +1 -1
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/hermes_provider/__init__.py +59 -40
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/processing.py +24 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/validation.py +8 -1
- {memleaf-0.2.18 → memleaf-0.2.19/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_extraction_quality_regressions.py +142 -1
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_hermes_provider.py +89 -2
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_pypi_install.py +5 -5
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.18 → memleaf-0.2.19}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/LICENSE +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/MANIFEST.in +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/examples/README.md +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/examples/basic_usage.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/install.ps1 +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/install.sh +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/setup.cfg +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/config.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/index.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/models.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/native_registration.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/prompts.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/service.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/__init__.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_cross_turn_dedupe_regressions.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_hermes_native_registration.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_host_events.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_install.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_todo_state_recovery.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_update_target_recovery.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.18 → memleaf-0.2.19}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.19 — 2026-09-03
|
|
6
|
+
|
|
7
|
+
- Keep Hermes `<untrusted_tool_result>` search audit status synchronized with
|
|
8
|
+
Core for `FOUND` and `NO_MATCH` results using bounded, strict decoding;
|
|
9
|
+
malformed and error envelopes remain audit errors.
|
|
10
|
+
- After three `mixed_future_use` gate responses, defer only the affected
|
|
11
|
+
candidate so valid candidates from the same turn can continue processing;
|
|
12
|
+
mixed content is never persisted and unrelated schema violations still fail.
|
|
13
|
+
|
|
5
14
|
## 0.2.18 — 2026-09-02
|
|
6
15
|
|
|
7
16
|
- 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.19
|
|
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.19。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.19 通过 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.19.**
|
|
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.19 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.19。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.19 通过 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.19"
|
|
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
|
|
@@ -387,47 +387,66 @@ def _scope_context(
|
|
|
387
387
|
def _hermes_search_status(value: Any) -> str:
|
|
388
388
|
"""Classify an observed MCP search result without retaining its content."""
|
|
389
389
|
|
|
390
|
-
|
|
391
|
-
if isinstance(decoded, list):
|
|
392
|
-
for item in decoded:
|
|
393
|
-
if isinstance(item, Mapping) and item.get("type") == "text":
|
|
394
|
-
decoded = item.get("text")
|
|
395
|
-
break
|
|
396
|
-
if isinstance(decoded, str):
|
|
397
|
-
try:
|
|
398
|
-
decoded = json.loads(decoded)
|
|
399
|
-
except (TypeError, ValueError):
|
|
400
|
-
return "error"
|
|
401
|
-
if not isinstance(decoded, Mapping):
|
|
402
|
-
return "error"
|
|
403
|
-
if decoded.get("isError") is True:
|
|
404
|
-
return "error"
|
|
405
|
-
nested = decoded.get("structuredContent")
|
|
406
|
-
if isinstance(nested, Mapping):
|
|
407
|
-
decoded = nested.get("result") if isinstance(nested.get("result"), Mapping) else nested
|
|
408
|
-
elif isinstance(decoded.get("content"), list):
|
|
409
|
-
return _hermes_search_status(decoded["content"])
|
|
410
|
-
if not isinstance(decoded, Mapping) or "error" in decoded:
|
|
411
|
-
return "error"
|
|
412
|
-
status = decoded.get("status")
|
|
413
|
-
results = decoded.get("results")
|
|
414
|
-
if status not in {"found", "no_match"} or not isinstance(results, list):
|
|
415
|
-
return "error"
|
|
416
|
-
valid_results = all(
|
|
417
|
-
isinstance(item, Mapping)
|
|
418
|
-
and set(item) == {"memory_id", "title"}
|
|
419
|
-
and isinstance(item.get("memory_id"), str)
|
|
420
|
-
and bool(item.get("memory_id"))
|
|
421
|
-
and isinstance(item.get("title"), str)
|
|
422
|
-
and bool(item.get("title"))
|
|
423
|
-
for item in results
|
|
424
|
-
)
|
|
425
|
-
if not valid_results:
|
|
390
|
+
if value is _CALL_FAILED or value is None:
|
|
426
391
|
return "error"
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
392
|
+
decoded: Any = _decode_hermes_tool_result(value)
|
|
393
|
+
for _ in range(_MAX_TOOL_RESULT_LAYERS):
|
|
394
|
+
if decoded is _MISSING_TOOL_RESULT:
|
|
395
|
+
return "error"
|
|
396
|
+
if isinstance(decoded, str):
|
|
397
|
+
decoded = _decode_hermes_tool_result(decoded)
|
|
398
|
+
continue
|
|
399
|
+
if isinstance(decoded, list):
|
|
400
|
+
text_item = next(
|
|
401
|
+
(
|
|
402
|
+
item
|
|
403
|
+
for item in decoded
|
|
404
|
+
if isinstance(item, Mapping) and item.get("type") == "text"
|
|
405
|
+
),
|
|
406
|
+
None,
|
|
407
|
+
)
|
|
408
|
+
if text_item is None:
|
|
409
|
+
return "error"
|
|
410
|
+
decoded = _decode_hermes_tool_result(text_item.get("text"))
|
|
411
|
+
continue
|
|
412
|
+
if not isinstance(decoded, Mapping):
|
|
413
|
+
return "error"
|
|
414
|
+
if decoded.get("isError") is True or decoded.get("error") is not None:
|
|
415
|
+
return "error"
|
|
416
|
+
|
|
417
|
+
status = decoded.get("status")
|
|
418
|
+
results = decoded.get("results")
|
|
419
|
+
if status is not None or results is not None:
|
|
420
|
+
if status not in {"found", "no_match"} or not isinstance(results, list):
|
|
421
|
+
return "error"
|
|
422
|
+
valid_results = all(
|
|
423
|
+
isinstance(item, Mapping)
|
|
424
|
+
and set(item) == {"memory_id", "title"}
|
|
425
|
+
and isinstance(item.get("memory_id"), str)
|
|
426
|
+
and bool(item.get("memory_id"))
|
|
427
|
+
and isinstance(item.get("title"), str)
|
|
428
|
+
and bool(item.get("title"))
|
|
429
|
+
for item in results
|
|
430
|
+
)
|
|
431
|
+
if not valid_results:
|
|
432
|
+
return "error"
|
|
433
|
+
if status == "found" and results:
|
|
434
|
+
return "found"
|
|
435
|
+
if status == "no_match" and not results:
|
|
436
|
+
return "no_match"
|
|
437
|
+
return "error"
|
|
438
|
+
|
|
439
|
+
nested = next(
|
|
440
|
+
(
|
|
441
|
+
decoded[key]
|
|
442
|
+
for key in ("structuredContent", "result", "content")
|
|
443
|
+
if isinstance(decoded.get(key), (Mapping, list, str))
|
|
444
|
+
),
|
|
445
|
+
_MISSING_TOOL_RESULT,
|
|
446
|
+
)
|
|
447
|
+
if nested is _MISSING_TOOL_RESULT:
|
|
448
|
+
return "error"
|
|
449
|
+
decoded = _decode_hermes_tool_result(nested)
|
|
431
450
|
return "error"
|
|
432
451
|
|
|
433
452
|
|
|
@@ -67,6 +67,7 @@ from .validation import (
|
|
|
67
67
|
normalize_relative_calendar_text,
|
|
68
68
|
is_aggregate_operational_text,
|
|
69
69
|
is_attachment_followup_only_text,
|
|
70
|
+
is_mixed_future_use_text,
|
|
70
71
|
is_project_plan_text,
|
|
71
72
|
)
|
|
72
73
|
from .vault import safe_component
|
|
@@ -2414,6 +2415,7 @@ class Processor:
|
|
|
2414
2415
|
related_memory_ids=gate_related_memory_ids,
|
|
2415
2416
|
scope_registry=validation_scope_registry,
|
|
2416
2417
|
enforce_model_scope_grounding=gate_attempt_count < 3,
|
|
2418
|
+
allow_mixed_future_use=gate_attempt_count >= 3,
|
|
2417
2419
|
)
|
|
2418
2420
|
if gate_attempt_count >= 3:
|
|
2419
2421
|
candidates = []
|
|
@@ -2564,6 +2566,19 @@ class Processor:
|
|
|
2564
2566
|
candidates.append(independent)
|
|
2565
2567
|
parsed = dict(parsed)
|
|
2566
2568
|
parsed["candidates"] = candidates
|
|
2569
|
+
if gate_attempt_count >= 3:
|
|
2570
|
+
# Mixed future-use is still a hard safety boundary. After
|
|
2571
|
+
# the bounded correction attempts, retain only the affected
|
|
2572
|
+
# candidate for deferred retry so valid siblings can commit;
|
|
2573
|
+
# no mixed body can reach summarize or persistence.
|
|
2574
|
+
marked_candidates: list[dict[str, Any]] = []
|
|
2575
|
+
for candidate in parsed["candidates"]:
|
|
2576
|
+
item = dict(candidate)
|
|
2577
|
+
if item.get("worth") and is_mixed_future_use_text(item.get("memory")):
|
|
2578
|
+
item["_defer_reason"] = "mixed_future_use"
|
|
2579
|
+
marked_candidates.append(item)
|
|
2580
|
+
parsed = dict(parsed)
|
|
2581
|
+
parsed["candidates"] = marked_candidates
|
|
2567
2582
|
return parsed
|
|
2568
2583
|
|
|
2569
2584
|
gate = self._complete_json_stage(
|
|
@@ -2647,6 +2662,15 @@ class Processor:
|
|
|
2647
2662
|
candidate_id_key = str(candidate.get("candidate_id", "")).casefold()
|
|
2648
2663
|
recovery = recovery_by_candidate.get(candidate_id_key)
|
|
2649
2664
|
detached_update_target_id = detached_update_target_ids.get(candidate_id_key)
|
|
2665
|
+
defer_reason = candidate.get("_defer_reason")
|
|
2666
|
+
if isinstance(defer_reason, str) and defer_reason == "mixed_future_use":
|
|
2667
|
+
self._defer_candidate(
|
|
2668
|
+
turn_ref,
|
|
2669
|
+
candidate,
|
|
2670
|
+
defer_reason,
|
|
2671
|
+
scopes=candidate.get("scopes"),
|
|
2672
|
+
)
|
|
2673
|
+
continue
|
|
2650
2674
|
if candidate.get("worth") and (
|
|
2651
2675
|
(read_only_query and recovery is None)
|
|
2652
2676
|
or _automatic_transient_memory(candidate.get("memory"))
|
|
@@ -1086,6 +1086,7 @@ def validate_gate_output(
|
|
|
1086
1086
|
related_memory_types: Mapping[str, Any] | None = None,
|
|
1087
1087
|
scope_registry: Mapping[str, Any] | None = None,
|
|
1088
1088
|
enforce_model_scope_grounding: bool = True,
|
|
1089
|
+
allow_mixed_future_use: bool = False,
|
|
1089
1090
|
) -> dict[str, Any]:
|
|
1090
1091
|
"""Validate and return a normalized gate object without writing anything."""
|
|
1091
1092
|
|
|
@@ -1146,7 +1147,11 @@ def validate_gate_output(
|
|
|
1146
1147
|
raise ModelOutputError("duplicate candidate cannot be worth remembering", validation_detail="invalid_flags")
|
|
1147
1148
|
if item["worth"] and candidate_type is None:
|
|
1148
1149
|
raise ModelOutputError("worth candidate must have a type", validation_detail="invalid_type")
|
|
1149
|
-
if
|
|
1150
|
+
if (
|
|
1151
|
+
item["worth"]
|
|
1152
|
+
and is_mixed_future_use_text(item["memory"])
|
|
1153
|
+
and not allow_mixed_future_use
|
|
1154
|
+
):
|
|
1150
1155
|
raise ModelOutputError(
|
|
1151
1156
|
"one candidate combines independent future uses",
|
|
1152
1157
|
validation_detail="mixed_future_use",
|
|
@@ -1263,6 +1268,7 @@ def parse_gate_output(
|
|
|
1263
1268
|
related_memory_types: Mapping[str, Any] | None = None,
|
|
1264
1269
|
scope_registry: Mapping[str, Any] | None = None,
|
|
1265
1270
|
enforce_model_scope_grounding: bool = True,
|
|
1271
|
+
allow_mixed_future_use: bool = False,
|
|
1266
1272
|
) -> dict[str, Any]:
|
|
1267
1273
|
try:
|
|
1268
1274
|
parsed = parse_strict_json(raw)
|
|
@@ -1279,6 +1285,7 @@ def parse_gate_output(
|
|
|
1279
1285
|
related_memory_types=related_memory_types,
|
|
1280
1286
|
scope_registry=scope_registry,
|
|
1281
1287
|
enforce_model_scope_grounding=enforce_model_scope_grounding,
|
|
1288
|
+
allow_mixed_future_use=allow_mixed_future_use,
|
|
1282
1289
|
)
|
|
1283
1290
|
except ModelOutputError as error:
|
|
1284
1291
|
if error.validation_reason is None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.19
|
|
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.19。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.19 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -14,7 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
|
|
15
15
|
from memleaf import Memleaf
|
|
16
16
|
from memleaf.index import event_key
|
|
17
|
-
from memleaf.validation import parse_gate_output
|
|
17
|
+
from memleaf.validation import ModelOutputError, parse_gate_output
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class QueueBackend:
|
|
@@ -199,6 +199,147 @@ class ExtractionQualityRegressionTests(unittest.TestCase):
|
|
|
199
199
|
"project constraint and deadline todo were merged into one memory",
|
|
200
200
|
)
|
|
201
201
|
|
|
202
|
+
def test_repeated_mixed_future_use_defers_only_bad_candidate(self):
|
|
203
|
+
"""A third mixed-use failure must not discard a valid sibling candidate."""
|
|
204
|
+
|
|
205
|
+
user_key, assistant_key = self.capture_turn(
|
|
206
|
+
"repeated-mixed-future-use",
|
|
207
|
+
user="请记录本轮确认的项目要求。",
|
|
208
|
+
assistant="系统采用达梦数据库;部署测试环境的任务必须在2026-09-10前完成。",
|
|
209
|
+
)
|
|
210
|
+
project_scope = ["project:金元顺安"]
|
|
211
|
+
mixed = candidate(
|
|
212
|
+
"mixed-repeated",
|
|
213
|
+
[user_key, assistant_key],
|
|
214
|
+
memory=(
|
|
215
|
+
"金元顺安实施计划采用达梦和东方通,并要求部署测试环境在"
|
|
216
|
+
"2026-09-10前完成。"
|
|
217
|
+
),
|
|
218
|
+
type="project",
|
|
219
|
+
scopes=project_scope,
|
|
220
|
+
)
|
|
221
|
+
valid = candidate(
|
|
222
|
+
"valid-sibling",
|
|
223
|
+
[user_key, assistant_key],
|
|
224
|
+
memory="本轮确认系统采用达梦数据库。",
|
|
225
|
+
type="fact",
|
|
226
|
+
scopes=["global"],
|
|
227
|
+
)
|
|
228
|
+
backend = QueueBackend(
|
|
229
|
+
[
|
|
230
|
+
gate([mixed, valid]),
|
|
231
|
+
gate([mixed, valid]),
|
|
232
|
+
gate([mixed, valid]),
|
|
233
|
+
summary(
|
|
234
|
+
user_key,
|
|
235
|
+
title="系统数据库选型",
|
|
236
|
+
body="本轮确认系统采用达梦数据库。",
|
|
237
|
+
type="fact",
|
|
238
|
+
scopes=["global"],
|
|
239
|
+
),
|
|
240
|
+
]
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
result = self.service.process(
|
|
244
|
+
source="hermes",
|
|
245
|
+
session_id="repeated-mixed-future-use",
|
|
246
|
+
model=backend,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.assertEqual(result["processed_turns"], 1)
|
|
250
|
+
self.assertEqual(result["memories_written"], 1)
|
|
251
|
+
self.assertEqual(len(self.active_memories()), 1)
|
|
252
|
+
self.assertEqual(self.active_memories()[0].body, "本轮确认系统采用达梦数据库。")
|
|
253
|
+
self.assertEqual(
|
|
254
|
+
[call["purpose"] for call in backend.calls],
|
|
255
|
+
["gate", "gate", "gate", "summarize"],
|
|
256
|
+
)
|
|
257
|
+
processed = json.loads(
|
|
258
|
+
self.service.vault.processed_index_path.read_text(encoding="utf-8")
|
|
259
|
+
)
|
|
260
|
+
state = processed["sessions"]["hermes/repeated-mixed-future-use"]
|
|
261
|
+
self.assertEqual(state["processing"]["status"], "idle")
|
|
262
|
+
self.assertEqual(result["deferred_candidates"], 1)
|
|
263
|
+
self.assertEqual(
|
|
264
|
+
state["processed_turns"][0]["deferred_candidates"][0]["candidate_id"],
|
|
265
|
+
"mixed-repeated",
|
|
266
|
+
)
|
|
267
|
+
self.assertEqual(
|
|
268
|
+
state["processed_turns"][0]["deferred_candidates"][0]["reason"],
|
|
269
|
+
"mixed_future_use",
|
|
270
|
+
)
|
|
271
|
+
self.assertTrue(
|
|
272
|
+
(
|
|
273
|
+
self.service.vault.inbox_path
|
|
274
|
+
/ "hermes"
|
|
275
|
+
/ "repeated-mixed-future-use.md"
|
|
276
|
+
).is_file()
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
retry_backend = QueueBackend(
|
|
280
|
+
[
|
|
281
|
+
gate([mixed, valid]),
|
|
282
|
+
gate([mixed, valid]),
|
|
283
|
+
gate([mixed, valid]),
|
|
284
|
+
summary(
|
|
285
|
+
user_key,
|
|
286
|
+
title="系统数据库选型",
|
|
287
|
+
body="本轮确认系统采用达梦数据库。",
|
|
288
|
+
type="fact",
|
|
289
|
+
scopes=["global"],
|
|
290
|
+
),
|
|
291
|
+
]
|
|
292
|
+
)
|
|
293
|
+
retry = self.service.process(
|
|
294
|
+
source="hermes",
|
|
295
|
+
session_id="repeated-mixed-future-use",
|
|
296
|
+
scope="global",
|
|
297
|
+
model=retry_backend,
|
|
298
|
+
)
|
|
299
|
+
self.assertEqual(retry["processed_turns"], 1)
|
|
300
|
+
self.assertEqual(retry["memory_ids"], [])
|
|
301
|
+
self.assertEqual(retry["memories_written"], 0)
|
|
302
|
+
self.assertEqual(len(self.active_memories()), 1)
|
|
303
|
+
self.assertEqual(retry["deferred_candidates"], 1)
|
|
304
|
+
|
|
305
|
+
def test_repeated_mixed_future_use_does_not_swallow_other_schema_errors(self):
|
|
306
|
+
"""The final mixed-use fallback must preserve unrelated validation failures."""
|
|
307
|
+
|
|
308
|
+
user_key, assistant_key = self.capture_turn(
|
|
309
|
+
"repeated-mixed-with-invalid-type",
|
|
310
|
+
user="请记录本轮确认的项目要求。",
|
|
311
|
+
assistant="系统采用达梦数据库;部署测试环境的任务必须在2026-09-10前完成。",
|
|
312
|
+
)
|
|
313
|
+
mixed = candidate(
|
|
314
|
+
"mixed-repeated",
|
|
315
|
+
[user_key, assistant_key],
|
|
316
|
+
memory=(
|
|
317
|
+
"金元顺安实施计划采用达梦和东方通,并要求部署测试环境在"
|
|
318
|
+
"2026-09-10前完成。"
|
|
319
|
+
),
|
|
320
|
+
type="project",
|
|
321
|
+
scopes=["project:金元顺安"],
|
|
322
|
+
)
|
|
323
|
+
invalid = candidate(
|
|
324
|
+
"invalid-sibling",
|
|
325
|
+
[user_key, assistant_key],
|
|
326
|
+
memory="本轮确认系统采用达梦数据库。",
|
|
327
|
+
type="unsupported-type",
|
|
328
|
+
scopes=["global"],
|
|
329
|
+
)
|
|
330
|
+
backend = QueueBackend([gate([mixed, invalid])] * 3)
|
|
331
|
+
|
|
332
|
+
with self.assertRaises(ModelOutputError) as raised:
|
|
333
|
+
self.service.process(
|
|
334
|
+
source="hermes",
|
|
335
|
+
session_id="repeated-mixed-with-invalid-type",
|
|
336
|
+
model=backend,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
self.assertEqual(getattr(raised.exception, "validation_detail", None), "invalid_type")
|
|
340
|
+
self.assertEqual([call["purpose"] for call in backend.calls], ["gate"] * 3)
|
|
341
|
+
self.assertEqual(self.active_memories(), [])
|
|
342
|
+
|
|
202
343
|
def test_fact_label_for_existing_project_plan_is_corrected_before_update(self):
|
|
203
344
|
"""A plan update keeps the active project type and exact target ID."""
|
|
204
345
|
|
|
@@ -303,11 +303,11 @@ class HermesProviderTests(unittest.TestCase):
|
|
|
303
303
|
|
|
304
304
|
def test_provider_does_not_warn_when_core_versions_match(self) -> None:
|
|
305
305
|
client = FakeClient(responses=[{"stats": True}])
|
|
306
|
-
client.server_version = "0.2.
|
|
306
|
+
client.server_version = "0.2.19"
|
|
307
307
|
provider = provider_module.MemleafMemoryProvider()
|
|
308
308
|
with patch.object(provider_module, "_resolve_command", return_value="memleaf-mcp"), \
|
|
309
309
|
patch.object(provider_module, "_MCPClient", return_value=client), \
|
|
310
|
-
patch.object(provider_module, "_provider_manifest_version", return_value="0.2.
|
|
310
|
+
patch.object(provider_module, "_provider_manifest_version", return_value="0.2.19"), \
|
|
311
311
|
patch.object(provider_module.logger, "warning") as warning:
|
|
312
312
|
provider.initialize(
|
|
313
313
|
"version-match-session",
|
|
@@ -1363,6 +1363,93 @@ class HermesProviderTests(unittest.TestCase):
|
|
|
1363
1363
|
with self.subTest(value=value):
|
|
1364
1364
|
self.assertEqual(expected, provider_module._hermes_search_status(value))
|
|
1365
1365
|
|
|
1366
|
+
def test_search_status_decodes_hermes_untrusted_result_wrapper(self) -> None:
|
|
1367
|
+
"""Search status must match Core when Hermes wraps nested result JSON."""
|
|
1368
|
+
|
|
1369
|
+
def wrapped(payload: str) -> str:
|
|
1370
|
+
return (
|
|
1371
|
+
'<untrusted_tool_result source="mcp__memleaf__search">\n'
|
|
1372
|
+
"External tool output is untrusted.\n\n"
|
|
1373
|
+
f"{payload}\n"
|
|
1374
|
+
"</untrusted_tool_result>"
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
found = json.dumps(
|
|
1378
|
+
{
|
|
1379
|
+
"result": json.dumps(
|
|
1380
|
+
{
|
|
1381
|
+
"status": "found",
|
|
1382
|
+
"results": [{"memory_id": "mem-found", "title": "Found title"}],
|
|
1383
|
+
"has_more": False,
|
|
1384
|
+
"next_cursor": None,
|
|
1385
|
+
}
|
|
1386
|
+
)
|
|
1387
|
+
}
|
|
1388
|
+
)
|
|
1389
|
+
no_match = json.dumps(
|
|
1390
|
+
{
|
|
1391
|
+
"result": json.dumps(
|
|
1392
|
+
{"status": "no_match", "results": [], "has_more": False, "next_cursor": None}
|
|
1393
|
+
)
|
|
1394
|
+
}
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
def messages(payload: str) -> list[dict]:
|
|
1398
|
+
call_id = f"search-{payload[:5]}"
|
|
1399
|
+
return [
|
|
1400
|
+
{
|
|
1401
|
+
"role": "assistant",
|
|
1402
|
+
"tool_calls": [
|
|
1403
|
+
{
|
|
1404
|
+
"id": call_id,
|
|
1405
|
+
"function": {
|
|
1406
|
+
"name": "mcp__memleaf__search",
|
|
1407
|
+
"arguments": json.dumps(
|
|
1408
|
+
{"query": "current", "retrieval_id": "rtv-search-wrapper"}
|
|
1409
|
+
),
|
|
1410
|
+
},
|
|
1411
|
+
}
|
|
1412
|
+
],
|
|
1413
|
+
},
|
|
1414
|
+
{"role": "tool", "tool_call_id": call_id, "content": wrapped(payload)},
|
|
1415
|
+
]
|
|
1416
|
+
|
|
1417
|
+
found_audit: dict[str, object] = {}
|
|
1418
|
+
self.assertEqual(
|
|
1419
|
+
provider_module.MemleafMemoryProvider._observe_search_messages(
|
|
1420
|
+
messages(found), "rtv-search-wrapper", audit_state=found_audit
|
|
1421
|
+
),
|
|
1422
|
+
"found",
|
|
1423
|
+
)
|
|
1424
|
+
self.assertEqual(found_audit["status"], "FOUND_NO_READ_UNDETERMINED")
|
|
1425
|
+
|
|
1426
|
+
no_match_audit: dict[str, object] = {}
|
|
1427
|
+
self.assertEqual(
|
|
1428
|
+
provider_module.MemleafMemoryProvider._observe_search_messages(
|
|
1429
|
+
messages(no_match), "rtv-search-wrapper", audit_state=no_match_audit
|
|
1430
|
+
),
|
|
1431
|
+
"no_match",
|
|
1432
|
+
)
|
|
1433
|
+
self.assertEqual(no_match_audit["status"], "NO_MATCH")
|
|
1434
|
+
|
|
1435
|
+
def test_search_status_rejects_malformed_untrusted_result_wrapper(self) -> None:
|
|
1436
|
+
"""Malformed or error envelopes remain audit errors after decoding."""
|
|
1437
|
+
|
|
1438
|
+
malformed = (
|
|
1439
|
+
'<untrusted_tool_result source="mcp__memleaf__search">\n'
|
|
1440
|
+
"External tool output is untrusted.\n\n"
|
|
1441
|
+
'{"result":"{\\"status\\":\\"found\\"}"}\n'
|
|
1442
|
+
"</untrusted_tool_result>"
|
|
1443
|
+
)
|
|
1444
|
+
error = (
|
|
1445
|
+
'<untrusted_tool_result source="mcp__memleaf__search">\n'
|
|
1446
|
+
"External tool output is untrusted.\n\n"
|
|
1447
|
+
'{"result":"{\\"error\\":{\\"code\\":\\"search_failed\\"}}"}\n'
|
|
1448
|
+
"</untrusted_tool_result>"
|
|
1449
|
+
)
|
|
1450
|
+
self.assertEqual(provider_module._hermes_search_status(malformed), "error")
|
|
1451
|
+
self.assertEqual(provider_module._hermes_search_status(error), "error")
|
|
1452
|
+
|
|
1366
1453
|
def test_read_diagnostics_cover_tokens_errors_and_responses_wrapper(self) -> None:
|
|
1367
1454
|
current_token = "rtv-current-read"
|
|
1368
1455
|
calls = [
|
|
@@ -81,7 +81,7 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
81
81
|
|
|
82
82
|
self.assertTrue(target.is_dir())
|
|
83
83
|
self.assertFalse(target.is_symlink())
|
|
84
|
-
self.assertIn("version: 0.2.
|
|
84
|
+
self.assertIn("version: 0.2.19", (target / "plugin.yaml").read_text(encoding="utf-8"))
|
|
85
85
|
|
|
86
86
|
def test_windows_hermes_paths_follow_official_native_layout(self) -> None:
|
|
87
87
|
with tempfile.TemporaryDirectory(prefix="memleaf-win-paths-") as temporary:
|
|
@@ -212,16 +212,16 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
212
212
|
"status": "configured",
|
|
213
213
|
"reason": "ok",
|
|
214
214
|
"vault": "/tmp/memleaf-vault",
|
|
215
|
-
"core_version": "0.2.
|
|
216
|
-
"provider_version": "0.2.
|
|
215
|
+
"core_version": "0.2.19",
|
|
216
|
+
"provider_version": "0.2.19",
|
|
217
217
|
"model": {"status": "configured"},
|
|
218
218
|
}
|
|
219
219
|
with mock.patch("memleaf.installer.install_hermes", return_value=result), \
|
|
220
220
|
mock.patch("sys.stdout") as stdout:
|
|
221
221
|
self.assertEqual(cli.main(["install"]), 0)
|
|
222
222
|
output = "\n".join(str(call.args[0]) for call in stdout.write.call_args_list)
|
|
223
|
-
self.assertIn("core=0.2.
|
|
224
|
-
self.assertIn("Hermes provider=0.2.
|
|
223
|
+
self.assertIn("core=0.2.19", output)
|
|
224
|
+
self.assertIn("Hermes provider=0.2.19", output)
|
|
225
225
|
|
|
226
226
|
def test_readmes_document_one_line_core_and_provider_upgrade(self) -> None:
|
|
227
227
|
command = "python -m pip install -U memleaf && python -m memleaf install"
|
|
@@ -104,7 +104,7 @@ class StageC1MCPTest(unittest.TestCase):
|
|
|
104
104
|
testcase.assertEqual(result["resultType"], "complete")
|
|
105
105
|
testcase.assertEqual(
|
|
106
106
|
result["_meta"]["io.modelcontextprotocol/serverInfo"],
|
|
107
|
-
{"name": "memleaf", "version": "0.2.
|
|
107
|
+
{"name": "memleaf", "version": "0.2.19"},
|
|
108
108
|
)
|
|
109
109
|
return result
|
|
110
110
|
|
|
@@ -24,7 +24,7 @@ class StageC3PackagingTests(unittest.TestCase):
|
|
|
24
24
|
|
|
25
25
|
def test_project_metadata_and_version_export(self) -> None:
|
|
26
26
|
self.assertEqual(self.project["name"], "memleaf")
|
|
27
|
-
self.assertEqual(self.project["version"], "0.2.
|
|
27
|
+
self.assertEqual(self.project["version"], "0.2.19")
|
|
28
28
|
self.assertEqual(self.project["requires-python"], ">=3.11")
|
|
29
29
|
self.assertEqual(self.project["dependencies"], [])
|
|
30
30
|
self.assertEqual(self.project["license"], "MIT")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|