memleaf 0.2.5__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.
Files changed (102) hide show
  1. {memleaf-0.2.5 → memleaf-0.2.7}/CHANGELOG.md +25 -0
  2. {memleaf-0.2.5/src/memleaf.egg-info → memleaf-0.2.7}/PKG-INFO +3 -3
  3. {memleaf-0.2.5 → memleaf-0.2.7}/README.md +2 -2
  4. {memleaf-0.2.5 → memleaf-0.2.7}/pyproject.toml +1 -1
  5. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/__init__.py +1 -1
  6. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/hermes_provider/__init__.py +2 -0
  7. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  8. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/processing.py +9 -0
  9. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/prompts.py +64 -4
  10. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/validation.py +27 -3
  11. {memleaf-0.2.5 → memleaf-0.2.7/src/memleaf.egg-info}/PKG-INFO +3 -3
  12. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_hermes_provider.py +34 -0
  13. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_pypi_install.py +1 -1
  14. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b1.py +118 -1
  15. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b2a.py +440 -1
  16. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_c1_mcp.py +1 -1
  17. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_c3_packaging.py +1 -1
  18. {memleaf-0.2.5 → memleaf-0.2.7}/IMPLEMENTATION_PLAN.md +0 -0
  19. {memleaf-0.2.5 → memleaf-0.2.7}/LICENSE +0 -0
  20. {memleaf-0.2.5 → memleaf-0.2.7}/MANIFEST.in +0 -0
  21. {memleaf-0.2.5 → memleaf-0.2.7}/README.en.md +0 -0
  22. {memleaf-0.2.5 → memleaf-0.2.7}/RELEASE_CHECKLIST.md +0 -0
  23. {memleaf-0.2.5 → memleaf-0.2.7}/examples/README.md +0 -0
  24. {memleaf-0.2.5 → memleaf-0.2.7}/examples/basic_usage.py +0 -0
  25. {memleaf-0.2.5 → memleaf-0.2.7}/examples/mcp_stdio.ndjson +0 -0
  26. {memleaf-0.2.5 → memleaf-0.2.7}/install.ps1 +0 -0
  27. {memleaf-0.2.5 → memleaf-0.2.7}/install.sh +0 -0
  28. {memleaf-0.2.5 → memleaf-0.2.7}/setup.cfg +0 -0
  29. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/__main__.py +0 -0
  30. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/adapters/__init__.py +0 -0
  31. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/adapters/antigravity.py +0 -0
  32. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/adapters/base.py +0 -0
  33. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/adapters/codex.py +0 -0
  34. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/adapters/hermes.py +0 -0
  35. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/budget.py +0 -0
  36. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/capture.py +0 -0
  37. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/cli.py +0 -0
  38. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/compaction.py +0 -0
  39. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/config.py +0 -0
  40. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/credentials.py +0 -0
  41. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/frontmatter.py +0 -0
  42. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/hermes_provider/README.md +0 -0
  43. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/host_events.py +0 -0
  44. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/host_runtime.py +0 -0
  45. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/inbox.py +0 -0
  46. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/index.py +0 -0
  47. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/installer.py +0 -0
  48. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/llm/__init__.py +0 -0
  49. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/llm/base.py +0 -0
  50. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/llm/claude_compatible.py +0 -0
  51. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/llm/gemini.py +0 -0
  52. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/llm/openai_compatible.py +0 -0
  53. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/llm/router.py +0 -0
  54. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/locking.py +0 -0
  55. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/mcp_server.py +0 -0
  56. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/memory_writer.py +0 -0
  57. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/model_discovery.py +0 -0
  58. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/models.py +0 -0
  59. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/native_index.py +0 -0
  60. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/redaction.py +0 -0
  61. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/retrieval.py +0 -0
  62. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/retrieval_gate.py +0 -0
  63. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/scope_maintenance.py +0 -0
  64. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/scope_state.py +0 -0
  65. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/service.py +0 -0
  66. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf/vault.py +0 -0
  67. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf.egg-info/SOURCES.txt +0 -0
  68. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf.egg-info/dependency_links.txt +0 -0
  69. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf.egg-info/entry_points.txt +0 -0
  70. {memleaf-0.2.5 → memleaf-0.2.7}/src/memleaf.egg-info/top_level.txt +0 -0
  71. {memleaf-0.2.5 → memleaf-0.2.7}/tests/__init__.py +0 -0
  72. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_admission_noise.py +0 -0
  73. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_codex_install.py +0 -0
  74. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_codex_native_cli.py +0 -0
  75. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_context_budget.py +0 -0
  76. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_credential_safety.py +0 -0
  77. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_cross_host_acceptance.py +0 -0
  78. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_hermes_stdio_transport.py +0 -0
  79. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_host_events.py +0 -0
  80. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_host_runtime_contract.py +0 -0
  81. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_install.py +0 -0
  82. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_maintenance_v2.py +0 -0
  83. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_model_discovery.py +0 -0
  84. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_retrieval_gate.py +0 -0
  85. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_retrieval_v2.py +0 -0
  86. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_session_lineage.py +0 -0
  87. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_a.py +0 -0
  88. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b2b.py +0 -0
  89. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3a_commit.py +0 -0
  90. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3a_contract.py +0 -0
  91. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3b_native_context.py +0 -0
  92. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3b_native_index.py +0 -0
  93. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3b_scope.py +0 -0
  94. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3c_retrieval.py +0 -0
  95. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  96. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_stage_c2_init.py +0 -0
  97. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_upgrade_preserves_vault.py +0 -0
  98. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_v2_gate_limits.py +0 -0
  99. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_v2_host_flow.py +0 -0
  100. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_v2_mcp_flow.py +0 -0
  101. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_v2_nomatch_semantics.py +0 -0
  102. {memleaf-0.2.5 → memleaf-0.2.7}/tests/test_v2_search_gate_acceptance.py +0 -0
@@ -2,6 +2,31 @@
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
+
17
+ ## 0.2.6 — 2026-09-01
18
+
19
+ - Reject one worthy memory that spans multiple independent project scopes and
20
+ retry with an explicit instruction to split actionable project topics while
21
+ discarding temporary mailbox-sweep status items.
22
+ - Tighten automatic admission so aggregate mailbox/daily-report snapshots,
23
+ one-off meetings without durable outcomes, pending PM acceptance, and
24
+ awaiting-feedback states do not become a cross-project permanent memory;
25
+ keep independent risks and actionable todos eligible.
26
+ - Prefer `UPDATE` or `NO_CHANGE` for an existing future-use topic, preserve
27
+ unimplemented customer requests as proposed or pending, and recognize the
28
+ Chinese relative-date forms `今日`, `明日`, and `昨日` in strict retries.
29
+
5
30
  ## 0.2.5 — 2026-09-01
6
31
 
7
32
  - Detect multiple gate candidates that target the same active memory before
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.5
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.5。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.5 通过 PyPI 分发。
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.5。**
8
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.5 通过 PyPI 分发。
7
+ > **当前版本:0.2.7。**
8
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.7 通过 PyPI 分发。
9
9
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
10
10
 
11
11
  ## 项目定位
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memleaf"
7
- version = "0.2.5"
7
+ version = "0.2.7"
8
8
  description = "A local-first Markdown memory core for AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  """Local-first Markdown memory core for AI agents."""
2
2
 
3
- __version__ = "0.2.5"
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
@@ -76,6 +76,8 @@ _MODEL_VALIDATION_DETAILS = frozenset(
76
76
  "candidate_shape",
77
77
  "duplicate_candidate_id",
78
78
  "duplicate_update_target",
79
+ "mixed_project_scopes",
80
+ "update_target_type_mismatch",
79
81
  "invalid_evidence",
80
82
  "invalid_flags",
81
83
  "invalid_type",
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.5
2
+ version: 0.2.7
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -32,12 +32,15 @@ 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,
37
+ MIXED_PROJECT_SCOPES_CORRECTION,
36
38
  RELATIVE_TIME_CORRECTION,
37
39
  GATE_SYSTEM,
38
40
  SUMMARY_TARGET_CORRECTION,
39
41
  SUMMARY_TYPE_CORRECTION,
40
42
  SUMMARIZE_SYSTEM,
43
+ UPDATE_TARGET_TYPE_CORRECTION,
41
44
  gate_prompt,
42
45
  summarize_prompt,
43
46
  )
@@ -479,6 +482,12 @@ class Processor:
479
482
  stage = getattr(error, "stage", None)
480
483
  if hint == "duplicate_update_target":
481
484
  return DUPLICATE_TARGET_CORRECTION
485
+ if hint == "mixed_project_scopes":
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
482
491
  if hint == "relative_time":
483
492
  return RELATIVE_TIME_CORRECTION
484
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
@@ -47,7 +52,7 @@ Calendar dates are strict. Each event may include an ISO-8601 UTC timestamp.
47
52
  For a one-off relative date in a candidate or its later summary, use the
48
53
  timestamp of the evidence event that supports it as the calendar anchor and
49
54
  emit an absolute YYYY-MM-DD date. Resolve today/tomorrow/yesterday,
50
- 今天/明天/昨天, 本周X/这周X/下周X/上周X, and this/next/last weekday;
55
+ 今天/明天/昨天/今日/明日/昨日, 本周X/这周X/下周X/上周X, and this/next/last weekday;
51
56
  use Monday-Sunday calendar weeks for week expressions. Do not leave these
52
57
  strong relative forms in a worthy memory, and never guess without a timestamp:
53
58
  defer or omit a date-dependent candidate if its supporting event has no
@@ -92,6 +97,13 @@ be independently retrieved and updated. Prefer one coherent current-state memory
92
97
  over adjacent overlapping snapshots; do not use a score or fixed category rule.
93
98
  Most ordinary turns should produce zero or one candidate; multiple require
94
99
  genuinely independent future questions or actions.
100
+ Never persist an aggregate mailbox sweep, daily report, or "items to watch"
101
+ digest as one cross-project memory. Inspect each item and split genuinely
102
+ independent project risks or actionable todo items; if an item has no
103
+ independent future use, set worth=false. A one-off meeting with no durable
104
+ decision or commitment, a request awaiting PM acceptance, and pending feedback
105
+ or another intermediate status normally have worth=false. Keep an independent
106
+ project risk or concrete future action even when it came from a digest.
95
107
  An intermediate state such as a draft awaiting confirmation, preparation/
96
108
  processing status, temporary error, repeated confirmation, or unconfirmed
97
109
  suggestion normally has no independent future use and is worth=false. Keep one
@@ -113,6 +125,11 @@ operational incident spans stats/search/remember calls; this default remains.
113
125
  Retain only a separate, user-confirmed future-use fact, preference, identity, constraint, commitment,
114
126
  project risk, or durable lesson that prevents repeating the investigation, not
115
127
  the execution transcript.
128
+ If a related active memory already covers the same future question/action and
129
+ the current turn adds no confirmed change, use worth=false and do not create a
130
+ sibling. If a customer or user proposes a new change without confirmation of
131
+ implementation, preserve it as requested/proposed/pending and never state that
132
+ it is already implemented.
116
133
  Return no prose, markdown fences, comments, or trailing text."""
117
134
 
118
135
 
@@ -142,11 +159,15 @@ to the gate candidate type and to the active update target type.
142
159
  For the same use, preserve still-valid facts, add confirmed progress, replace
143
160
  contradictions, and never reduce the memory to the latest operation or create a
144
161
  sibling with a new label.
162
+ Never turn an email sweep, daily report, or watchlist into one cross-project
163
+ summary. Keep only one future-use topic in the summary; split unrelated
164
+ projects at the gate, and omit temporary meeting/awaiting-acceptance/pending-
165
+ feedback states unless they have an independent future action.
145
166
  Calendar dates are strict. Each event may include an ISO-8601 UTC timestamp.
146
167
  For a one-off relative date in the summary title or body, use the timestamp of
147
168
  the supporting evidence event as the calendar anchor and emit an absolute
148
169
  YYYY-MM-DD date. Resolve today/tomorrow/yesterday,
149
- 今天/明天/昨天, 本周X/这周X/下周X/上周X, and this/next/last weekday;
170
+ 今天/明天/昨天/今日/明日/昨日, 本周X/这周X/下周X/上周X, and this/next/last weekday;
150
171
  use Monday-Sunday calendar weeks for week expressions. Never leave these
151
172
  strong relative forms in the summary, and never guess without a timestamp.
152
173
  Recurring schedules such as 每天、每周三, or every Wednesday are not one-off
@@ -155,7 +176,12 @@ In automatic capture/process mode, a pure read-only query whose answer only
155
176
  restates a related active memory is not a new candidate. It must have been
156
177
  rejected by the gate; do not use duplicate_memory_id or update_memory_id to
157
178
  append the query as a source. Only a newly confirmed fact or state change in
158
- the current evidence may update an existing memory.
179
+ the current evidence may update an existing memory. If a related active memory
180
+ already covers the same future question/action and the current evidence adds no
181
+ confirmed change, retain the existing memory unchanged (the gate should emit
182
+ worth=false) rather than creating a sibling. When the current evidence is a
183
+ customer/user request that is not confirmed implemented, state it as
184
+ requested/proposed/pending and never as completed or deployed.
159
185
  fields and JSON types are title (string), body (string), tags (string list),
160
186
  type (one of preference, fact, project, todo, event, identity, other), scopes
161
187
  (non-empty string list), and sources (non-empty object list). scope_source, if
@@ -210,7 +236,7 @@ RELATIVE_TIME_CORRECTION = (
210
236
  "each one-off relative date from that timestamp and the stated weekday "
211
237
  "semantics. In the summary title and body, every one-off calendar date must "
212
238
  "be written only as YYYY-MM-DD: remove forms such as today/tomorrow/yesterday, "
213
- "今天/明天/昨天, 本周三/这周三/下周三/上周三, and this/next/last Wednesday. "
239
+ "今天/明天/昨天/今日/明日/昨日, 本周三/这周三/下周三/上周三, and this/next/last Wednesday. "
214
240
  "If a relative weekday is followed by a parenthesized numeric date and they "
215
241
  "conflict, trust the event timestamp plus the weekday meaning, replace the "
216
242
  "numeric date with the computed YYYY-MM-DD, and remove the relative wording "
@@ -220,6 +246,40 @@ RELATIVE_TIME_CORRECTION = (
220
246
  )
221
247
 
222
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
+
270
+ MIXED_PROJECT_SCOPES_CORRECTION = (
271
+ "Previous output violated: mixed_project_scopes. Keep each worthy "
272
+ "candidate and summary atomic: it may contain at most one distinct "
273
+ "project:<name> scope. At the gate, split genuinely independent projects "
274
+ "into separate candidates, preserving their own evidence, or mark purely "
275
+ "temporary meeting/awaiting-acceptance/pending-feedback items worth=false. "
276
+ "In a summary, keep only one future-use project topic and its matching "
277
+ "details. global plus one project and valid domain/portfolio parent scopes "
278
+ "are allowed; do not remove those parent scopes merely to pass validation. "
279
+ "Return only the strict JSON object."
280
+ )
281
+
282
+
223
283
  DUPLICATE_TARGET_CORRECTION = (
224
284
  "Previous output violated: duplicate_update_target. Merge every candidate "
225
285
  "that refers to the same active memory target into one candidate for one "
@@ -52,6 +52,8 @@ MODEL_VALIDATION_DETAILS = frozenset(
52
52
  "candidate_shape",
53
53
  "duplicate_candidate_id",
54
54
  "duplicate_update_target",
55
+ "mixed_project_scopes",
56
+ "update_target_type_mismatch",
55
57
  "invalid_evidence",
56
58
  "invalid_flags",
57
59
  "invalid_type",
@@ -72,7 +74,7 @@ _RELATIVE_CALENDAR_EXPRESSION = re.compile(
72
74
  r"(?<![A-Za-z])(?:today|tomorrow|yesterday)(?![A-Za-z])"
73
75
  r"|(?<![A-Za-z])(?:this|next|last)\s+(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday)(?![A-Za-z])"
74
76
  r"|(?:本|这|下|上)(?:个)?(?:周|星期|礼拜)\s*(?:一|二|三|四|五|六|日|天|末|[1-7])"
75
- r"|(?:今天|明天|昨天)"
77
+ r"|(?:今天|明天|昨天|今日|明日|昨日)"
76
78
  r")",
77
79
  re.IGNORECASE,
78
80
  )
@@ -228,6 +230,25 @@ def _scopes(value: Any, scope_source: Any) -> list[str]:
228
230
  return result
229
231
 
230
232
 
233
+ def _reject_mixed_project_scopes(scopes: Iterable[str]) -> None:
234
+ """Keep one worthy memory atomic across independent project scopes.
235
+
236
+ Parent scopes (global/domain/portfolio) may accompany one project scope;
237
+ only two or more distinct project scopes represent an ambiguous aggregate.
238
+ """
239
+
240
+ project_scopes = {
241
+ scope.casefold()
242
+ for scope in scopes
243
+ if isinstance(scope, str) and scope.partition(":")[0] == "project"
244
+ }
245
+ if len(project_scopes) > 1:
246
+ raise ModelOutputError(
247
+ "one memory cannot cover multiple project scopes",
248
+ validation_detail="mixed_project_scopes",
249
+ )
250
+
251
+
231
252
  def _memory_id(value: Any, field: str) -> str:
232
253
  result = _string(value, field)
233
254
  if "/" in result or "\\" in result or result in (".", ".."):
@@ -520,7 +541,7 @@ def validate_gate_output(
520
541
  if target_type is not None and candidate_type != target_type:
521
542
  raise ModelOutputError(
522
543
  "candidate type does not match update target",
523
- validation_detail="invalid_type",
544
+ validation_detail="update_target_type_mismatch",
524
545
  )
525
546
  item["update_memory_id"] = update_id
526
547
  for target_field in ("duplicate_memory_id", "update_memory_id"):
@@ -535,7 +556,9 @@ def validate_gate_output(
535
556
  target_ids.add(target_key)
536
557
  if not isinstance(item["scope_source"], str) or item["scope_source"] not in SCOPE_SOURCES:
537
558
  raise ModelOutputError("invalid scope_source", validation_detail="invalid_scope_source")
538
- _scopes(item["scopes"], item["scope_source"])
559
+ item["scopes"] = _scopes(item["scopes"], item["scope_source"])
560
+ if item["worth"]:
561
+ _reject_mixed_project_scopes(item["scopes"])
539
562
  if "reason" in item:
540
563
  _string(item["reason"], "reason", nonempty=False)
541
564
  if len(item["reason"]) > 30:
@@ -659,6 +682,7 @@ def validate_summarize_output(
659
682
  validation_detail="invalid_type",
660
683
  )
661
684
  item["scopes"] = _scopes(item["scopes"], item.get("scope_source"))
685
+ _reject_mixed_project_scopes(item["scopes"])
662
686
  item["scope_operations"] = _scope_operations(
663
687
  item.get("scope_operations"),
664
688
  summary_scopes=item["scopes"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.5
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.5。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.5 通过 PyPI 分发。
26
+ > **当前版本:0.2.7。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.7 通过 PyPI 分发。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
30
30
  ## 项目定位
@@ -1734,6 +1734,40 @@ class HermesProviderTests(unittest.TestCase):
1734
1734
  self.assertIsNotNone(fields)
1735
1735
  self.assertEqual(fields[4], "duplicate_update_target")
1736
1736
 
1737
+ def test_mixed_project_scope_validation_detail_is_preserved(self) -> None:
1738
+ value = {
1739
+ "isError": True,
1740
+ "structuredContent": {
1741
+ "error": {
1742
+ "code": "model_invalid_response",
1743
+ "stage": "gate",
1744
+ "validation_reason": "schema_violation",
1745
+ "validation_detail": "mixed_project_scopes",
1746
+ "attempt_count": 3,
1747
+ }
1748
+ },
1749
+ }
1750
+ fields = provider_module._mcp_error_fields(value)
1751
+ self.assertIsNotNone(fields)
1752
+ self.assertEqual(fields[4], "mixed_project_scopes")
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
+
1737
1771
  def test_relative_time_validation_detail_is_preserved_and_logged(self) -> None:
1738
1772
  value = {
1739
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.5", (target / "plugin.yaml").read_text(encoding="utf-8"))
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,11 +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,
16
+ MIXED_PROJECT_SCOPES_CORRECTION,
15
17
  RELATIVE_TIME_CORRECTION,
16
18
  SUMMARY_TARGET_CORRECTION,
17
19
  SUMMARY_TYPE_CORRECTION,
18
20
  SUMMARIZE_SYSTEM,
21
+ UPDATE_TARGET_TYPE_CORRECTION,
19
22
  gate_prompt,
20
23
  summarize_prompt,
21
24
  )
@@ -184,6 +187,7 @@ class StageB1Test(unittest.TestCase):
184
187
  self.assertIn("ISO-8601 UTC timestamp", prompt_system)
185
188
  self.assertIn("YYYY-MM-DD", prompt_system)
186
189
  self.assertIn("本周X/这周X/下周X/上周X", prompt_system)
190
+ self.assertIn("今日/明日/昨日", prompt_system)
187
191
  self.assertIn("every Wednesday", prompt_system)
188
192
 
189
193
  def test_relative_summary_is_rejected_then_absolute_retry_is_committed(self):
@@ -1098,6 +1102,20 @@ class ValidationTest(unittest.TestCase):
1098
1102
  ]
1099
1103
  }
1100
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
+ )
1101
1119
  self.assertIsNone(ModelOutputError("secret", validation_detail="not-a-detail").validation_detail)
1102
1120
 
1103
1121
  def test_gate_rejects_reused_active_target_and_checks_target_type(self):
@@ -1135,7 +1153,28 @@ class ValidationTest(unittest.TestCase):
1135
1153
  related_memory_ids=["mem-target"],
1136
1154
  related_memory_types={"mem-target": "fact"},
1137
1155
  )
1138
- self.assertEqual(raised.exception.validation_detail, "invalid_type")
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")
1139
1178
 
1140
1179
  def test_summary_constraints_preserve_gate_target_and_type(self):
1141
1180
  summary = {
@@ -1178,6 +1217,16 @@ class ValidationTest(unittest.TestCase):
1178
1217
  self.assertIn("only once", DUPLICATE_TARGET_CORRECTION.lower())
1179
1218
  self.assertIn("immutable", SUMMARY_TARGET_CORRECTION.lower())
1180
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())
1225
+
1226
+ def test_correction_prompt_explains_project_scope_atomicity(self):
1227
+ self.assertIn("mixed_project_scopes", MIXED_PROJECT_SCOPES_CORRECTION)
1228
+ self.assertIn("split", MIXED_PROJECT_SCOPES_CORRECTION.lower())
1229
+ self.assertIn("global plus one project", MIXED_PROJECT_SCOPES_CORRECTION.lower())
1181
1230
 
1182
1231
  def test_mcp_model_diagnostics_preserve_duplicate_target_detail(self):
1183
1232
  error = ModelOutputError(
@@ -1190,6 +1239,17 @@ class ValidationTest(unittest.TestCase):
1190
1239
  "duplicate_update_target",
1191
1240
  )
1192
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
+
1193
1253
  def test_strict_json_rejects_fences_tail_and_summary_requires_atomic_fields(self):
1194
1254
  for raw in ("```json\n{\"candidates\":[]}\n```", '{"candidates":[]} trailing'):
1195
1255
  with self.assertRaises(ModelOutputError):
@@ -1331,6 +1391,63 @@ class ValidationTest(unittest.TestCase):
1331
1391
  ["event-a"],
1332
1392
  )
1333
1393
 
1394
+ def test_worthy_memory_rejects_multiple_project_scopes_but_allows_parent_context(self):
1395
+ base = {
1396
+ "candidate_id": "c",
1397
+ "memory": "one future-use topic",
1398
+ "evidence_event_ids": ["event-a"],
1399
+ "duplicate": False,
1400
+ "worth": True,
1401
+ "type": "project",
1402
+ "scope_source": "model",
1403
+ }
1404
+ invalid = dict(base, scopes=["project:cn", "project:fund"])
1405
+ with self.assertRaises(ModelOutputError) as raised:
1406
+ parse_gate_output(json.dumps({"candidates": [invalid]}), ["event-a"])
1407
+ self.assertEqual(raised.exception.validation_detail, "mixed_project_scopes")
1408
+
1409
+ for scopes in (
1410
+ ["global", "project:cn"],
1411
+ ["portfolio:finance", "project:cn"],
1412
+ ["domain:work", "portfolio:finance", "project:cn"],
1413
+ ):
1414
+ with self.subTest(scopes=scopes):
1415
+ parsed = parse_gate_output(
1416
+ json.dumps({"candidates": [dict(base, scopes=scopes)]}),
1417
+ ["event-a"],
1418
+ )
1419
+ self.assertEqual(parsed["candidates"][0]["scopes"], scopes)
1420
+
1421
+ summary = {
1422
+ "title": "One topic",
1423
+ "body": "One future-use topic.",
1424
+ "tags": [],
1425
+ "type": "project",
1426
+ "scopes": ["project:cn", "project:fund"],
1427
+ "scope_source": "model",
1428
+ "sources": [{"event_key": "event-a"}],
1429
+ }
1430
+ with self.assertRaises(ModelOutputError) as raised:
1431
+ parse_summarize_output(json.dumps(summary), current_event_keys=["event-a"])
1432
+ self.assertEqual(raised.exception.validation_detail, "mixed_project_scopes")
1433
+
1434
+ def test_chinese_relative_date_aliases_are_rejected(self):
1435
+ for relative in ("今日", "明日", "昨日"):
1436
+ with self.subTest(relative=relative):
1437
+ summary = {
1438
+ "title": f"截止{relative}",
1439
+ "body": "完成项目交付。",
1440
+ "tags": [],
1441
+ "type": "todo",
1442
+ "scopes": ["global"],
1443
+ "scope_source": "model",
1444
+ "sources": [{"event_key": "event-a"}],
1445
+ "status": "active",
1446
+ }
1447
+ with self.assertRaises(ModelOutputError) as raised:
1448
+ parse_summarize_output(json.dumps(summary), current_event_keys=["event-a"])
1449
+ self.assertEqual(raised.exception.validation_detail, "relative_time")
1450
+
1334
1451
  def test_summary_evidence_and_sources_are_limited_to_current_turn_fields(self):
1335
1452
  summary = {
1336
1453
  "title": "Traceable",