memleaf 0.2.6__tar.gz → 0.2.7__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.6 → memleaf-0.2.7}/CHANGELOG.md +12 -0
- {memleaf-0.2.6/src/memleaf.egg-info → memleaf-0.2.7}/PKG-INFO +3 -3
- {memleaf-0.2.6 → memleaf-0.2.7}/README.md +2 -2
- {memleaf-0.2.6 → memleaf-0.2.7}/pyproject.toml +1 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/hermes_provider/__init__.py +1 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/processing.py +6 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/prompts.py +26 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/validation.py +2 -1
- {memleaf-0.2.6 → memleaf-0.2.7/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_hermes_provider.py +17 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_pypi_install.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b1.py +54 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b2a.py +93 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.6 → memleaf-0.2.7}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/LICENSE +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/MANIFEST.in +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/README.en.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/examples/README.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/examples/basic_usage.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/install.ps1 +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/install.sh +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/setup.cfg +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/config.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/index.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/models.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/service.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/__init__.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_host_events.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_install.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.6 → memleaf-0.2.7}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.7 — 2026-09-01
|
|
6
|
+
|
|
7
|
+
- Distinguish an invalid gate type enum from a candidate type that conflicts
|
|
8
|
+
with its selected active update target, while preserving the strict,
|
|
9
|
+
immutable target-type constraint.
|
|
10
|
+
- Give each gate failure a bounded, actionable retry instruction: use a legal
|
|
11
|
+
memory type, preserve the target type for the same future use, or split a
|
|
12
|
+
genuinely different future use into an independent atomic candidate.
|
|
13
|
+
- Preserve the dedicated `update_target_type_mismatch` diagnostic across Core,
|
|
14
|
+
MCP, and Hermes; repeated failures still write nothing and retain the inbox
|
|
15
|
+
for a later retry.
|
|
16
|
+
|
|
5
17
|
## 0.2.6 — 2026-09-01
|
|
6
18
|
|
|
7
19
|
- Reject one worthy memory that spans multiple independent project scopes and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
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.7。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.7 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -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.7。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.7 通过 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.7"
|
|
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
|
|
@@ -32,6 +32,7 @@ from .models import Memory, utc_now
|
|
|
32
32
|
from .native_index import NativeIndexer
|
|
33
33
|
from .prompts import (
|
|
34
34
|
DUPLICATE_TARGET_CORRECTION,
|
|
35
|
+
GATE_TYPE_CORRECTION,
|
|
35
36
|
JSON_CORRECTION,
|
|
36
37
|
MIXED_PROJECT_SCOPES_CORRECTION,
|
|
37
38
|
RELATIVE_TIME_CORRECTION,
|
|
@@ -39,6 +40,7 @@ from .prompts import (
|
|
|
39
40
|
SUMMARY_TARGET_CORRECTION,
|
|
40
41
|
SUMMARY_TYPE_CORRECTION,
|
|
41
42
|
SUMMARIZE_SYSTEM,
|
|
43
|
+
UPDATE_TARGET_TYPE_CORRECTION,
|
|
42
44
|
gate_prompt,
|
|
43
45
|
summarize_prompt,
|
|
44
46
|
)
|
|
@@ -482,6 +484,10 @@ class Processor:
|
|
|
482
484
|
return DUPLICATE_TARGET_CORRECTION
|
|
483
485
|
if hint == "mixed_project_scopes":
|
|
484
486
|
return MIXED_PROJECT_SCOPES_CORRECTION
|
|
487
|
+
if stage == "gate" and hint == "update_target_type_mismatch":
|
|
488
|
+
return UPDATE_TARGET_TYPE_CORRECTION
|
|
489
|
+
if stage == "gate" and hint == "invalid_type":
|
|
490
|
+
return GATE_TYPE_CORRECTION
|
|
485
491
|
if hint == "relative_time":
|
|
486
492
|
return RELATIVE_TIME_CORRECTION
|
|
487
493
|
if stage == "summarize" and hint == "invalid_update_target":
|
|
@@ -34,6 +34,11 @@ useful. Related active memories only identify a complete duplicate or valid
|
|
|
34
34
|
state-update target; they are not current evidence and cannot make a non-worthy
|
|
35
35
|
operational/test result worthy. Their item, body, and serialized-character
|
|
36
36
|
context is bounded; ellipses and omissions do not prove a fact is absent.
|
|
37
|
+
An update target's existing type is immutable: when the same future use is being
|
|
38
|
+
updated, candidate type must exactly equal the related active target type. If a
|
|
39
|
+
fact serves a different future use, do not force it onto that target; omit
|
|
40
|
+
update_memory_id and emit a separate atomic candidate, or use worth=false when
|
|
41
|
+
it is already covered or only temporary.
|
|
37
42
|
Within one gate response, each active memory target may appear at most once
|
|
38
43
|
across all candidates, whether it is named by duplicate_memory_id or
|
|
39
44
|
update_memory_id. If multiple facts share one future-use/update target, merge
|
|
@@ -241,6 +246,27 @@ RELATIVE_TIME_CORRECTION = (
|
|
|
241
246
|
)
|
|
242
247
|
|
|
243
248
|
|
|
249
|
+
GATE_TYPE_CORRECTION = (
|
|
250
|
+
"Previous output violated: invalid_type. In gate output, whenever type is "
|
|
251
|
+
"non-null it must be exactly one of preference, fact, project, todo, event, "
|
|
252
|
+
"identity, or other. worth=true requires a non-null legal type; worth=false "
|
|
253
|
+
"may use null or any of those legal types (for example, a duplicate candidate "
|
|
254
|
+
"may retain its type). Do not invent enum values such as requirement, "
|
|
255
|
+
"decision, or task. Return only the strict gate JSON object."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
UPDATE_TARGET_TYPE_CORRECTION = (
|
|
260
|
+
"Previous output violated: update_target_type_mismatch. An existing active "
|
|
261
|
+
"update target's type is immutable. For the same future-use topic, set the "
|
|
262
|
+
"candidate type exactly equal to the supplied related target type. For a "
|
|
263
|
+
"different future use, remove update_memory_id and emit a separate atomic "
|
|
264
|
+
"candidate, or set worth=false if it is already covered or only temporary. "
|
|
265
|
+
"Use only a supplied related active ID; never guess, use native/history IDs, "
|
|
266
|
+
"or force a cross-purpose update. Return only the strict gate JSON object."
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
244
270
|
MIXED_PROJECT_SCOPES_CORRECTION = (
|
|
245
271
|
"Previous output violated: mixed_project_scopes. Keep each worthy "
|
|
246
272
|
"candidate and summary atomic: it may contain at most one distinct "
|
|
@@ -53,6 +53,7 @@ MODEL_VALIDATION_DETAILS = frozenset(
|
|
|
53
53
|
"duplicate_candidate_id",
|
|
54
54
|
"duplicate_update_target",
|
|
55
55
|
"mixed_project_scopes",
|
|
56
|
+
"update_target_type_mismatch",
|
|
56
57
|
"invalid_evidence",
|
|
57
58
|
"invalid_flags",
|
|
58
59
|
"invalid_type",
|
|
@@ -540,7 +541,7 @@ def validate_gate_output(
|
|
|
540
541
|
if target_type is not None and candidate_type != target_type:
|
|
541
542
|
raise ModelOutputError(
|
|
542
543
|
"candidate type does not match update target",
|
|
543
|
-
validation_detail="
|
|
544
|
+
validation_detail="update_target_type_mismatch",
|
|
544
545
|
)
|
|
545
546
|
item["update_memory_id"] = update_id
|
|
546
547
|
for target_field in ("duplicate_memory_id", "update_memory_id"):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
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.7。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.7 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -1751,6 +1751,23 @@ class HermesProviderTests(unittest.TestCase):
|
|
|
1751
1751
|
self.assertIsNotNone(fields)
|
|
1752
1752
|
self.assertEqual(fields[4], "mixed_project_scopes")
|
|
1753
1753
|
|
|
1754
|
+
def test_update_target_type_validation_detail_is_preserved(self) -> None:
|
|
1755
|
+
value = {
|
|
1756
|
+
"isError": True,
|
|
1757
|
+
"structuredContent": {
|
|
1758
|
+
"error": {
|
|
1759
|
+
"code": "model_invalid_response",
|
|
1760
|
+
"stage": "gate",
|
|
1761
|
+
"validation_reason": "schema_violation",
|
|
1762
|
+
"validation_detail": "update_target_type_mismatch",
|
|
1763
|
+
"attempt_count": 3,
|
|
1764
|
+
}
|
|
1765
|
+
},
|
|
1766
|
+
}
|
|
1767
|
+
fields = provider_module._mcp_error_fields(value)
|
|
1768
|
+
self.assertIsNotNone(fields)
|
|
1769
|
+
self.assertEqual(fields[4], "update_target_type_mismatch")
|
|
1770
|
+
|
|
1754
1771
|
def test_relative_time_validation_detail_is_preserved_and_logged(self) -> None:
|
|
1755
1772
|
value = {
|
|
1756
1773
|
"isError": True,
|
|
@@ -39,7 +39,7 @@ class PyPIInstallTests(unittest.TestCase):
|
|
|
39
39
|
|
|
40
40
|
self.assertTrue(target.is_dir())
|
|
41
41
|
self.assertFalse(target.is_symlink())
|
|
42
|
-
self.assertIn("version: 0.2.
|
|
42
|
+
self.assertIn("version: 0.2.7", (target / "plugin.yaml").read_text(encoding="utf-8"))
|
|
43
43
|
|
|
44
44
|
def test_windows_hermes_paths_follow_official_native_layout(self) -> None:
|
|
45
45
|
with tempfile.TemporaryDirectory(prefix="memleaf-win-paths-") as temporary:
|
|
@@ -11,12 +11,14 @@ from memleaf.config import default_config, load_config, save_config
|
|
|
11
11
|
from memleaf.inbox import complete_turns, parse_inbox
|
|
12
12
|
from memleaf.prompts import (
|
|
13
13
|
DUPLICATE_TARGET_CORRECTION,
|
|
14
|
+
GATE_TYPE_CORRECTION,
|
|
14
15
|
GATE_SYSTEM,
|
|
15
16
|
MIXED_PROJECT_SCOPES_CORRECTION,
|
|
16
17
|
RELATIVE_TIME_CORRECTION,
|
|
17
18
|
SUMMARY_TARGET_CORRECTION,
|
|
18
19
|
SUMMARY_TYPE_CORRECTION,
|
|
19
20
|
SUMMARIZE_SYSTEM,
|
|
21
|
+
UPDATE_TARGET_TYPE_CORRECTION,
|
|
20
22
|
gate_prompt,
|
|
21
23
|
summarize_prompt,
|
|
22
24
|
)
|
|
@@ -1100,6 +1102,20 @@ class ValidationTest(unittest.TestCase):
|
|
|
1100
1102
|
]
|
|
1101
1103
|
}
|
|
1102
1104
|
self.assertEqual(len(parse_gate_output(json.dumps(valid), ["event-a"])["candidates"]), 3)
|
|
1105
|
+
nonworthy_with_type = dict(
|
|
1106
|
+
base,
|
|
1107
|
+
candidate_id="duplicate-observation",
|
|
1108
|
+
duplicate=True,
|
|
1109
|
+
worth=False,
|
|
1110
|
+
type="fact",
|
|
1111
|
+
)
|
|
1112
|
+
self.assertEqual(
|
|
1113
|
+
parse_gate_output(
|
|
1114
|
+
json.dumps({"candidates": [nonworthy_with_type]}),
|
|
1115
|
+
["event-a"],
|
|
1116
|
+
)["candidates"][0]["type"],
|
|
1117
|
+
"fact",
|
|
1118
|
+
)
|
|
1103
1119
|
self.assertIsNone(ModelOutputError("secret", validation_detail="not-a-detail").validation_detail)
|
|
1104
1120
|
|
|
1105
1121
|
def test_gate_rejects_reused_active_target_and_checks_target_type(self):
|
|
@@ -1137,7 +1153,28 @@ class ValidationTest(unittest.TestCase):
|
|
|
1137
1153
|
related_memory_ids=["mem-target"],
|
|
1138
1154
|
related_memory_types={"mem-target": "fact"},
|
|
1139
1155
|
)
|
|
1140
|
-
self.assertEqual(raised.exception.validation_detail, "
|
|
1156
|
+
self.assertEqual(raised.exception.validation_detail, "update_target_type_mismatch")
|
|
1157
|
+
|
|
1158
|
+
def test_gate_update_target_type_mismatch_has_dedicated_detail(self):
|
|
1159
|
+
candidate = {
|
|
1160
|
+
"candidate_id": "wrong-target-type",
|
|
1161
|
+
"memory": "A project update",
|
|
1162
|
+
"evidence_event_ids": ["event-a"],
|
|
1163
|
+
"duplicate": False,
|
|
1164
|
+
"worth": True,
|
|
1165
|
+
"type": "project",
|
|
1166
|
+
"scopes": ["global"],
|
|
1167
|
+
"scope_source": "model",
|
|
1168
|
+
"update_memory_id": "mem-fact",
|
|
1169
|
+
}
|
|
1170
|
+
with self.assertRaises(ModelOutputError) as raised:
|
|
1171
|
+
parse_gate_output(
|
|
1172
|
+
json.dumps({"candidates": [candidate]}),
|
|
1173
|
+
current_event_keys=["event-a"],
|
|
1174
|
+
related_memory_ids=["mem-fact"],
|
|
1175
|
+
related_memory_types={"mem-fact": "fact"},
|
|
1176
|
+
)
|
|
1177
|
+
self.assertEqual(raised.exception.validation_detail, "update_target_type_mismatch")
|
|
1141
1178
|
|
|
1142
1179
|
def test_summary_constraints_preserve_gate_target_and_type(self):
|
|
1143
1180
|
summary = {
|
|
@@ -1180,6 +1217,11 @@ class ValidationTest(unittest.TestCase):
|
|
|
1180
1217
|
self.assertIn("only once", DUPLICATE_TARGET_CORRECTION.lower())
|
|
1181
1218
|
self.assertIn("immutable", SUMMARY_TARGET_CORRECTION.lower())
|
|
1182
1219
|
self.assertIn("exactly equal", SUMMARY_TYPE_CORRECTION.lower())
|
|
1220
|
+
self.assertIn("invalid_type", GATE_TYPE_CORRECTION)
|
|
1221
|
+
self.assertIn("worth=false", GATE_TYPE_CORRECTION)
|
|
1222
|
+
self.assertIn("may use null or any of those legal types", GATE_TYPE_CORRECTION)
|
|
1223
|
+
self.assertIn("update_target_type_mismatch", UPDATE_TARGET_TYPE_CORRECTION)
|
|
1224
|
+
self.assertIn("immutable", UPDATE_TARGET_TYPE_CORRECTION.lower())
|
|
1183
1225
|
|
|
1184
1226
|
def test_correction_prompt_explains_project_scope_atomicity(self):
|
|
1185
1227
|
self.assertIn("mixed_project_scopes", MIXED_PROJECT_SCOPES_CORRECTION)
|
|
@@ -1197,6 +1239,17 @@ class ValidationTest(unittest.TestCase):
|
|
|
1197
1239
|
"duplicate_update_target",
|
|
1198
1240
|
)
|
|
1199
1241
|
|
|
1242
|
+
def test_mcp_model_diagnostics_preserve_update_target_type_detail(self):
|
|
1243
|
+
error = ModelOutputError(
|
|
1244
|
+
"safe",
|
|
1245
|
+
validation_reason="schema_violation",
|
|
1246
|
+
validation_detail="update_target_type_mismatch",
|
|
1247
|
+
)
|
|
1248
|
+
self.assertEqual(
|
|
1249
|
+
_safe_model_diagnostics(error)["validation_detail"],
|
|
1250
|
+
"update_target_type_mismatch",
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1200
1253
|
def test_strict_json_rejects_fences_tail_and_summary_requires_atomic_fields(self):
|
|
1201
1254
|
for raw in ("```json\n{\"candidates\":[]}\n```", '{"candidates":[]} trailing'):
|
|
1202
1255
|
with self.assertRaises(ModelOutputError):
|
|
@@ -725,11 +725,102 @@ class StageB2ATest(unittest.TestCase):
|
|
|
725
725
|
self.assertEqual(result["processed_turns"], 1)
|
|
726
726
|
self.assertEqual(result["memories_written"], 1)
|
|
727
727
|
self.assertEqual([call["purpose"] for call in backend.calls], ["gate", "gate", "summarize", "summarize"])
|
|
728
|
-
self.assertIn("Previous output violated:
|
|
728
|
+
self.assertIn("Previous output violated: update_target_type_mismatch.", backend.calls[1]["prompt"])
|
|
729
|
+
self.assertIn("existing active update target's type is immutable", backend.calls[1]["prompt"])
|
|
729
730
|
self.assertIn("Previous output violated: invalid_type.", backend.calls[3]["prompt"])
|
|
730
731
|
self.assertEqual(service.read(existing.memory_id).body, "The fact is updated.")
|
|
731
732
|
self.assertEqual(len(service.vault.list_markdown("history")), 1)
|
|
732
733
|
|
|
734
|
+
def test_different_future_use_omits_update_target_and_creates_independent_memory(self):
|
|
735
|
+
backend = QueueBackend()
|
|
736
|
+
service = self.service(backend, name="independent-future-use")
|
|
737
|
+
existing = service.create_memory(
|
|
738
|
+
memory_id="mem-existing-fact",
|
|
739
|
+
title="Existing fact",
|
|
740
|
+
body="The existing fact remains unchanged.",
|
|
741
|
+
type="fact",
|
|
742
|
+
scopes=["global"],
|
|
743
|
+
)
|
|
744
|
+
user_key, _ = self.capture_turn(
|
|
745
|
+
service,
|
|
746
|
+
turn="independent-future-use",
|
|
747
|
+
user_event="independent-future-use-user",
|
|
748
|
+
assistant_event="independent-future-use-assistant",
|
|
749
|
+
user="A separate project needs a deployment checklist.",
|
|
750
|
+
assistant="The checklist is a new future action.",
|
|
751
|
+
)
|
|
752
|
+
candidate = self.candidate(
|
|
753
|
+
"independent-project-todo",
|
|
754
|
+
[user_key],
|
|
755
|
+
memory="Prepare a deployment checklist for a separate project.",
|
|
756
|
+
type="todo",
|
|
757
|
+
)
|
|
758
|
+
candidate["scopes"] = ["project:separate"]
|
|
759
|
+
backend.responses.extend(
|
|
760
|
+
[
|
|
761
|
+
self.gate([candidate]),
|
|
762
|
+
self.summary(
|
|
763
|
+
user_key,
|
|
764
|
+
title="Deployment checklist",
|
|
765
|
+
body="Prepare a deployment checklist for a separate project.",
|
|
766
|
+
type="todo",
|
|
767
|
+
scopes=["project:separate"],
|
|
768
|
+
status="active",
|
|
769
|
+
),
|
|
770
|
+
]
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
result = service.process()
|
|
774
|
+
|
|
775
|
+
self.assertEqual(result["memories_written"], 1)
|
|
776
|
+
self.assertEqual(service.read(existing.memory_id).body, existing.body)
|
|
777
|
+
self.assertEqual(len(service.vault.list_markdown("history")), 0)
|
|
778
|
+
active = self.knowledge(service)
|
|
779
|
+
self.assertEqual(len(active), 2)
|
|
780
|
+
independent = next(record.memory for record in active if record.memory.memory_id != existing.memory_id)
|
|
781
|
+
self.assertEqual(independent.type, "todo")
|
|
782
|
+
self.assertEqual(independent.scopes, ["project:separate"])
|
|
783
|
+
|
|
784
|
+
def test_three_gate_update_target_type_mismatches_keep_queue_and_write_nothing(self):
|
|
785
|
+
backend = QueueBackend()
|
|
786
|
+
service = self.service(backend, name="update-target-type-failure")
|
|
787
|
+
existing = service.create_memory(
|
|
788
|
+
memory_id="mem-fact-target",
|
|
789
|
+
title="Existing fact",
|
|
790
|
+
body="The existing fact.",
|
|
791
|
+
type="fact",
|
|
792
|
+
scopes=["global"],
|
|
793
|
+
)
|
|
794
|
+
user_key, _ = self.capture_turn(
|
|
795
|
+
service,
|
|
796
|
+
turn="update-target-type-failure",
|
|
797
|
+
user_event="update-target-type-failure-user",
|
|
798
|
+
assistant_event="update-target-type-failure-assistant",
|
|
799
|
+
)
|
|
800
|
+
wrong_gate = self.candidate(
|
|
801
|
+
"wrong-target-type",
|
|
802
|
+
[user_key],
|
|
803
|
+
memory="A project update forced onto a fact target.",
|
|
804
|
+
type="project",
|
|
805
|
+
update_memory_id=existing.memory_id,
|
|
806
|
+
)
|
|
807
|
+
backend.responses.extend([self.gate([wrong_gate])] * 3)
|
|
808
|
+
|
|
809
|
+
with self.assertRaises(ModelOutputError) as raised:
|
|
810
|
+
service.process()
|
|
811
|
+
|
|
812
|
+
self.assertEqual(raised.exception.validation_detail, "update_target_type_mismatch")
|
|
813
|
+
self.assertEqual(raised.exception.stage, "gate")
|
|
814
|
+
self.assertEqual(raised.exception.attempt_count, 3)
|
|
815
|
+
marker = self.processed(service)["sessions"]["codex/s"]["processing"]
|
|
816
|
+
self.assertEqual(marker["failure_stage"], "gate")
|
|
817
|
+
self.assertEqual(marker["validation_detail"], "update_target_type_mismatch")
|
|
818
|
+
self.assertEqual(marker["attempt_count"], 3)
|
|
819
|
+
self.assertEqual(self.processed(service)["sessions"]["codex/s"].get("watermark", 0), 0)
|
|
820
|
+
self.assertTrue((service.vault.inbox_path / "codex" / "s.md").is_file())
|
|
821
|
+
self.assertEqual(service.read(existing.memory_id).body, existing.body)
|
|
822
|
+
self.assertEqual(service._read_memories_unlocked("history"), [])
|
|
823
|
+
|
|
733
824
|
def test_mixed_project_summary_retries_before_writing(self):
|
|
734
825
|
backend = QueueBackend()
|
|
735
826
|
service = self.service(backend, name="mixed-summary-retry")
|
|
@@ -1184,6 +1275,7 @@ class StageB2ATest(unittest.TestCase):
|
|
|
1184
1275
|
|
|
1185
1276
|
self.assertEqual(result["processed_turns"], 1)
|
|
1186
1277
|
self.assertIn("Previous output violated: invalid_type.", backend.calls[1]["prompt"])
|
|
1278
|
+
self.assertIn("whenever type is non-null", backend.calls[1]["prompt"])
|
|
1187
1279
|
self.assertNotIn(secret, backend.calls[1]["prompt"])
|
|
1188
1280
|
|
|
1189
1281
|
def test_empty_fenced_and_missing_gate_shapes_retry_once(self):
|
|
@@ -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.7"},
|
|
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.7")
|
|
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
|