memleaf 0.2.11__tar.gz → 0.2.12__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.11 → memleaf-0.2.12}/CHANGELOG.md +14 -0
  2. {memleaf-0.2.11/src/memleaf.egg-info → memleaf-0.2.12}/PKG-INFO +3 -3
  3. {memleaf-0.2.11 → memleaf-0.2.12}/README.en.md +2 -2
  4. {memleaf-0.2.11 → memleaf-0.2.12}/README.md +2 -2
  5. {memleaf-0.2.11 → memleaf-0.2.12}/pyproject.toml +1 -1
  6. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/__init__.py +1 -1
  7. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/hermes_provider/plugin.yaml +1 -1
  8. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/processing.py +119 -0
  9. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/prompts.py +17 -0
  10. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/validation.py +54 -0
  11. {memleaf-0.2.11 → memleaf-0.2.12/src/memleaf.egg-info}/PKG-INFO +3 -3
  12. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_admission_noise.py +53 -0
  13. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_hermes_provider.py +4 -4
  14. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_pypi_install.py +5 -5
  15. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b1.py +48 -1
  16. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b2a.py +187 -0
  17. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_c1_mcp.py +1 -1
  18. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_c3_packaging.py +1 -1
  19. {memleaf-0.2.11 → memleaf-0.2.12}/IMPLEMENTATION_PLAN.md +0 -0
  20. {memleaf-0.2.11 → memleaf-0.2.12}/LICENSE +0 -0
  21. {memleaf-0.2.11 → memleaf-0.2.12}/MANIFEST.in +0 -0
  22. {memleaf-0.2.11 → memleaf-0.2.12}/RELEASE_CHECKLIST.md +0 -0
  23. {memleaf-0.2.11 → memleaf-0.2.12}/examples/README.md +0 -0
  24. {memleaf-0.2.11 → memleaf-0.2.12}/examples/basic_usage.py +0 -0
  25. {memleaf-0.2.11 → memleaf-0.2.12}/examples/mcp_stdio.ndjson +0 -0
  26. {memleaf-0.2.11 → memleaf-0.2.12}/install.ps1 +0 -0
  27. {memleaf-0.2.11 → memleaf-0.2.12}/install.sh +0 -0
  28. {memleaf-0.2.11 → memleaf-0.2.12}/setup.cfg +0 -0
  29. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/__main__.py +0 -0
  30. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/adapters/__init__.py +0 -0
  31. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/adapters/antigravity.py +0 -0
  32. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/adapters/base.py +0 -0
  33. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/adapters/codex.py +0 -0
  34. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/adapters/hermes.py +0 -0
  35. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/budget.py +0 -0
  36. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/capture.py +0 -0
  37. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/cli.py +0 -0
  38. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/compaction.py +0 -0
  39. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/config.py +0 -0
  40. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/credentials.py +0 -0
  41. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/frontmatter.py +0 -0
  42. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/hermes_provider/README.md +0 -0
  43. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/hermes_provider/__init__.py +0 -0
  44. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/host_events.py +0 -0
  45. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/host_runtime.py +0 -0
  46. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/inbox.py +0 -0
  47. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/index.py +0 -0
  48. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/installer.py +0 -0
  49. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/llm/__init__.py +0 -0
  50. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/llm/base.py +0 -0
  51. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/llm/claude_compatible.py +0 -0
  52. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/llm/gemini.py +0 -0
  53. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/llm/openai_compatible.py +0 -0
  54. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/llm/router.py +0 -0
  55. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/locking.py +0 -0
  56. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/mcp_server.py +0 -0
  57. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/memory_writer.py +0 -0
  58. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/model_discovery.py +0 -0
  59. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/models.py +0 -0
  60. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/native_index.py +0 -0
  61. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/redaction.py +0 -0
  62. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/retrieval.py +0 -0
  63. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/retrieval_gate.py +0 -0
  64. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/scope_maintenance.py +0 -0
  65. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/scope_state.py +0 -0
  66. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/service.py +0 -0
  67. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf/vault.py +0 -0
  68. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf.egg-info/SOURCES.txt +0 -0
  69. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf.egg-info/dependency_links.txt +0 -0
  70. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf.egg-info/entry_points.txt +0 -0
  71. {memleaf-0.2.11 → memleaf-0.2.12}/src/memleaf.egg-info/top_level.txt +0 -0
  72. {memleaf-0.2.11 → memleaf-0.2.12}/tests/__init__.py +0 -0
  73. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_codex_install.py +0 -0
  74. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_codex_native_cli.py +0 -0
  75. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_context_budget.py +0 -0
  76. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_credential_safety.py +0 -0
  77. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_cross_host_acceptance.py +0 -0
  78. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_hermes_stdio_transport.py +0 -0
  79. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_host_events.py +0 -0
  80. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_host_runtime_contract.py +0 -0
  81. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_install.py +0 -0
  82. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_maintenance_v2.py +0 -0
  83. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_model_discovery.py +0 -0
  84. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_retrieval_gate.py +0 -0
  85. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_retrieval_v2.py +0 -0
  86. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_session_lineage.py +0 -0
  87. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_a.py +0 -0
  88. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b2b.py +0 -0
  89. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3a_commit.py +0 -0
  90. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3a_contract.py +0 -0
  91. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3b_native_context.py +0 -0
  92. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3b_native_index.py +0 -0
  93. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3b_scope.py +0 -0
  94. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3c_retrieval.py +0 -0
  95. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_b3d_scope_maintenance.py +0 -0
  96. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_stage_c2_init.py +0 -0
  97. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_upgrade_preserves_vault.py +0 -0
  98. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_v2_gate_limits.py +0 -0
  99. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_v2_host_flow.py +0 -0
  100. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_v2_mcp_flow.py +0 -0
  101. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_v2_nomatch_semantics.py +0 -0
  102. {memleaf-0.2.11 → memleaf-0.2.12}/tests/test_v2_search_gate_acceptance.py +0 -0
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to memleaf are documented here.
4
4
 
5
+ ## 0.2.12 — 2026-09-02
6
+
7
+ - Remove empty ASCII or Chinese parentheses left after an absolute date is
8
+ normalized, so permanent memory never stores values such as
9
+ `2026-09-03()`.
10
+ - Reconcile a same-scoped implementation-plan candidate with its unique active
11
+ project-plan memory even when the model labels the candidate as a fact or
12
+ omits `update_memory_id`; preserve the target's immutable type and history.
13
+ - Exclude sent-mail, attachment, archive, and meeting records from automatic
14
+ plan-target inference, and defer instead of guessing when multiple project
15
+ plan targets remain.
16
+ - Reject combined mailbox or daily-report count/status digests as permanent
17
+ memory while keeping separately emitted atomic actions eligible.
18
+
5
19
  ## 0.2.11 — 2026-09-02
6
20
 
7
21
  - Document one complete macOS/Linux upgrade command that upgrades the core
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.11
3
+ Version: 0.2.12
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.11。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.11 通过 PyPI 分发。
26
+ > **当前版本:0.2.12。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.12 通过 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.11.**
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.11 is distributed through PyPI.
7
+ > **Version: 0.2.12.**
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.12 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.11。**
8
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.11 通过 PyPI 分发。
7
+ > **当前版本:0.2.12。**
8
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.12 通过 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.11"
7
+ version = "0.2.12"
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.11"
3
+ __version__ = "0.2.12"
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
@@ -1,5 +1,5 @@
1
1
  name: memleaf
2
- version: 0.2.11
2
+ version: 0.2.12
3
3
  description: "Hermes-native external MemoryProvider for local-first Markdown memory shared with the memleaf MCP server."
4
4
  hooks:
5
5
  - prefetch
@@ -64,6 +64,7 @@ from .validation import (
64
64
  parse_strict_json,
65
65
  parse_summarize_output,
66
66
  normalize_relative_calendar_text,
67
+ is_aggregate_operational_text,
67
68
  )
68
69
  from .vault import safe_component
69
70
 
@@ -1682,6 +1683,114 @@ class Processor:
1682
1683
  # parser has already limited the target to an active related memory.
1683
1684
  return candidate_scope_source in {"user", "session_context"}
1684
1685
 
1686
+ @staticmethod
1687
+ def _project_scope_keys(scopes: Any) -> set[str]:
1688
+ if not isinstance(scopes, list):
1689
+ return set()
1690
+ return {
1691
+ scope.casefold()
1692
+ for scope in scopes
1693
+ if isinstance(scope, str) and scope.startswith("project:")
1694
+ }
1695
+
1696
+ @staticmethod
1697
+ def _is_project_plan_title(value: Any) -> bool:
1698
+ text = normalize_term(value) if isinstance(value, str) else ""
1699
+ return any(
1700
+ marker in text
1701
+ for marker in ("实施计划", "项目计划", "implementation plan", "project plan")
1702
+ )
1703
+
1704
+ @staticmethod
1705
+ def _is_adjacent_plan_record(value: Any) -> bool:
1706
+ text = normalize_term(value) if isinstance(value, str) else ""
1707
+ return any(
1708
+ marker in text
1709
+ for marker in (
1710
+ "已发送", "发送", "邮件", "附件", "存档", "会议", "启动会", "纪要",
1711
+ "sent", "email", "mail", "attachment", "archive", "meeting", "minutes",
1712
+ )
1713
+ )
1714
+
1715
+ def _infer_update_target(
1716
+ self,
1717
+ candidate: Mapping[str, Any],
1718
+ related: Iterable[Mapping[str, Any]],
1719
+ ) -> dict[str, Any]:
1720
+ """Reuse one unambiguously matching project plan/constraint memory.
1721
+
1722
+ The gate remains authoritative when it names a target. This narrow
1723
+ fallback handles a common model omission: a project-plan candidate is
1724
+ emitted as a new ``fact`` without ``update_memory_id`` even though the
1725
+ same scoped plan is already active. The active target's type is the
1726
+ canonical type; it is copied before summary validation so the target
1727
+ type invariant remains enforced.
1728
+ """
1729
+
1730
+ result = dict(candidate)
1731
+ if (
1732
+ not result.get("worth")
1733
+ or result.get("duplicate")
1734
+ or any(result.get(field) for field in ("duplicate_memory_id", "update_memory_id"))
1735
+ or not isinstance(result.get("memory"), str)
1736
+ ):
1737
+ return result
1738
+ if result.get("type") not in {"fact", "project"}:
1739
+ return result
1740
+ if not self._is_project_plan_title(result["memory"]):
1741
+ return result
1742
+ project_keys = self._project_scope_keys(result.get("scopes"))
1743
+ if len(project_keys) != 1:
1744
+ return result
1745
+
1746
+ project_matches: list[Memory] = []
1747
+ fact_matches: list[Memory] = []
1748
+ seen: set[str] = set()
1749
+ for item in related:
1750
+ if not isinstance(item, Mapping) or item.get("native") is True:
1751
+ continue
1752
+ memory_id = item.get("memory_id")
1753
+ item_type = item.get("type")
1754
+ item_scopes = item.get("scopes")
1755
+ if (
1756
+ not isinstance(memory_id, str)
1757
+ or not isinstance(item_type, str)
1758
+ or item_type not in {"fact", "project"}
1759
+ or self._project_scope_keys(item_scopes) != project_keys
1760
+ or memory_id.casefold() in seen
1761
+ or not self._is_project_plan_title(item.get("title"))
1762
+ or self._is_adjacent_plan_record(item.get("title"))
1763
+ ):
1764
+ continue
1765
+ try:
1766
+ target = Memory.from_mapping(item)
1767
+ except (TypeError, ValueError):
1768
+ continue
1769
+ seen.add(memory_id.casefold())
1770
+ if target.type == "project":
1771
+ project_matches.append(target)
1772
+ else:
1773
+ fact_matches.append(target)
1774
+
1775
+ # Prefer a project target over a same-topic fact. If there are
1776
+ # multiple project targets, defer rather than creating a sibling by
1777
+ # guessing; the same conservative rule applies when only multiple
1778
+ # durable fact targets remain.
1779
+ if len(project_matches) > 1 or (not project_matches and len(fact_matches) > 1):
1780
+ result["_defer_reason"] = "ambiguous_update_target"
1781
+ return result
1782
+ if project_matches:
1783
+ target = project_matches[0]
1784
+ elif len(fact_matches) == 1:
1785
+ target = fact_matches[0]
1786
+ else:
1787
+ return result
1788
+ result["update_memory_id"] = target.memory_id
1789
+ # The update target's type is immutable. Correct a model's fact-vs-
1790
+ # project label only after the same-use target was identified.
1791
+ result["type"] = target.type
1792
+ return result
1793
+
1685
1794
  def _defer_candidate(
1686
1795
  self,
1687
1796
  turn_ref: tuple[str, str, str],
@@ -2026,6 +2135,11 @@ class Processor:
2026
2135
  requests: list[dict[str, Any]] = []
2027
2136
  observed_scopes: list[str] = []
2028
2137
  for candidate in gate["candidates"]:
2138
+ # A combined mailbox/daily digest is not an atomic memory. If a
2139
+ # concrete action was worth retaining, the gate must emit it as
2140
+ # its own candidate; the aggregate shell itself is NO_CHANGE.
2141
+ if candidate.get("worth") and is_aggregate_operational_text(candidate.get("memory")):
2142
+ continue
2029
2143
  candidate_scopes = list(candidate["scopes"])
2030
2144
  # An automatic candidate with no reliable project attribution is
2031
2145
  # retained as a retryable inbox turn, never silently promoted to
@@ -2075,6 +2189,11 @@ class Processor:
2075
2189
  priority_only=scope_directory is not None,
2076
2190
  scope_records=scoped_records if scope_directory is not None else None,
2077
2191
  )
2192
+ candidate = self._infer_update_target(candidate, candidate_related)
2193
+ defer_reason = candidate.pop("_defer_reason", None)
2194
+ if defer_reason:
2195
+ self._defer_candidate(turn_ref, candidate, defer_reason)
2196
+ continue
2078
2197
  candidate_native_ids = [item["native_id"] for item in candidate_native_refs]
2079
2198
  candidate_memory_ids = [
2080
2199
  item["memory_id"]
@@ -141,6 +141,16 @@ the current turn adds no confirmed change, use worth=false and do not create a
141
141
  sibling. If a customer or user proposes a new change without confirmation of
142
142
  implementation, preserve it as requested/proposed/pending and never state that
143
143
  it is already implemented.
144
+ When a candidate names a project plan or project constraint already represented
145
+ by one same-scoped active memory, prefer that exact active memory as
146
+ update_memory_id, even when the candidate's initial type label is fact; the
147
+ active target type is immutable and must be copied. Never create a sibling for
148
+ the same future question/action. Prefer the durable project-plan target over
149
+ same-topic sent-mail, attachment/archive, or meeting records; if multiple
150
+ same-use project targets remain, do not guess an update target. A candidate
151
+ that still combines a mailbox or daily digest shell with counts/statuses is not
152
+ atomic: set worth=false for the aggregate, and emit any independent concrete
153
+ action as a separate candidate.
144
154
  Return no prose, markdown fences, comments, or trailing text."""
145
155
 
146
156
 
@@ -243,6 +253,13 @@ surrounding diagnosis into a memory. If a user-confirmed future-use
243
253
  preference, identity, constraint, project risk, or durable lesson appears in
244
254
  the same evidence, summarize only that future-use topic. In explicit remember mode,
245
255
  summarize only the requested object and do not append tool/test diagnostics.
256
+ If the gate candidate points to a same-scoped existing plan or constraint,
257
+ update that exact target and retain its still-valid content; do not create a
258
+ new sibling merely because the gate called it a fact. Prefer a durable project
259
+ plan target over same-topic sent-mail, attachment/archive, or meeting records;
260
+ the target type remains immutable. Never summarize a combined mailbox/daily
261
+ digest shell with counts or statuses; return NO_CHANGE for that aggregate,
262
+ while a separate atomic future-use action may still be summarized.
246
263
  Return no prose, markdown fences, comments, or trailing text."""
247
264
 
248
265
 
@@ -104,6 +104,10 @@ _ISO_CALENDAR_DATE = re.compile(
104
104
  r"(?<![A-Za-z\d./-])\d{4}-(?:0?[1-9]|1[0-2])-(?:0?[1-9]|[12]\d|3[01])"
105
105
  r"(?![A-Za-z\d./-])"
106
106
  )
107
+ _EMPTY_ISO_DATE_PARENTHESIS = re.compile(
108
+ r"(?P<date>\d{4}-(?:0?[1-9]|1[0-2])-(?:0?[1-9]|[12]\d|3[01]))"
109
+ r"[ \t]*(?:\([ \t]*[))]|([ \t]*[))])"
110
+ )
107
111
  _DUPLICATE_ISO_DATE = re.compile(
108
112
  r"(?P<date>\d{4}-\d{2}-\d{2})"
109
113
  r"\s*[,,、:]?\s*(?:(?:就是|即|即为|即是|也就是|是|为)|[//])\s*"
@@ -275,6 +279,14 @@ def _collapse_duplicate_calendar_dates(value: str) -> str:
275
279
  return value
276
280
 
277
281
 
282
+ def _strip_empty_iso_date_parenthesis(value: str) -> str:
283
+ """Remove empty ASCII/Chinese parentheses attached to an ISO date."""
284
+
285
+ if not isinstance(value, str):
286
+ return value
287
+ return _EMPTY_ISO_DATE_PARENTHESIS.sub(r"\g<date>", value)
288
+
289
+
278
290
  def _normalize_relative_calendar_text(
279
291
  text: str,
280
292
  anchor: Any,
@@ -328,6 +340,7 @@ def _normalize_relative_calendar_text(
328
340
  normalized = _RELATIVE_CALENDAR_EXPRESSION.sub(replace_token, text)
329
341
  if relative_replaced:
330
342
  normalized = _collapse_duplicate_calendar_dates(normalized)
343
+ normalized = _strip_empty_iso_date_parenthesis(normalized)
331
344
  return normalized, safe
332
345
 
333
346
 
@@ -345,6 +358,43 @@ def normalize_relative_calendar_text(text: str, anchor: Any) -> str | None:
345
358
  return normalized if safe else None
346
359
 
347
360
 
361
+ _AGGREGATE_DIGEST_MARKERS = (
362
+ "日报",
363
+ "汇总",
364
+ "巡检",
365
+ "收件箱",
366
+ "邮箱巡检",
367
+ "daily report",
368
+ "digest",
369
+ "watchlist",
370
+ "mailbox sweep",
371
+ "inbox sweep",
372
+ )
373
+ _AGGREGATE_COUNT = re.compile(
374
+ r"(?:\d+\s*(?:条|项|个|件|封|tasks?|items?)|(?:完成|待受理|派发|新增|逾期)\s*\d+)"
375
+ r"|(?:completed|pending|assigned|new|overdue)\s*[::]?\s*\d+",
376
+ re.IGNORECASE,
377
+ )
378
+
379
+
380
+ def is_aggregate_operational_text(value: Any) -> bool:
381
+ """Recognize a combined operational digest, not one concrete work item.
382
+
383
+ This deliberately requires multiple aggregate signals. A single overdue
384
+ task reported by a daily scan remains eligible for the model's normal
385
+ future-use decision; a multi-count sweep must be split before persistence.
386
+ """
387
+
388
+ if not isinstance(value, str):
389
+ return False
390
+ folded = value.casefold()
391
+ if not any(marker in folded for marker in _AGGREGATE_DIGEST_MARKERS):
392
+ return False
393
+ count_signals = len(_AGGREGATE_COUNT.findall(value))
394
+ separator_count = len(re.findall(r"[;;]|[—-].*[;;]", value))
395
+ return count_signals >= 2 or (count_signals >= 1 and separator_count >= 1)
396
+
397
+
348
398
  def _reject_constant(value: str) -> None:
349
399
  raise ModelOutputError("non-finite JSON number is not allowed", validation_reason="invalid_json")
350
400
 
@@ -1022,6 +1072,9 @@ def validate_summarize_output(
1022
1072
  }
1023
1073
  _require_keys(value, {"title", "body", "tags", "type", "scopes", "sources"}, allowed)
1024
1074
  item = dict(value)
1075
+ for field in ("title", "body"):
1076
+ if isinstance(item.get(field), str):
1077
+ item[field] = _strip_empty_iso_date_parenthesis(item[field])
1025
1078
  _string(item["title"], "title")
1026
1079
  _string(item["body"], "body", multiline=True)
1027
1080
  _reject_relative_calendar_expression(item)
@@ -1281,6 +1334,7 @@ __all__ = [
1281
1334
  "parse_gate_output",
1282
1335
  "parse_compact_output",
1283
1336
  "parse_strict_json",
1337
+ "is_aggregate_operational_text",
1284
1338
  "normalize_relative_calendar_text",
1285
1339
  "parse_summarize_output",
1286
1340
  "parse_summary_output",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memleaf
3
- Version: 0.2.11
3
+ Version: 0.2.12
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.11。**
27
- > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.11 通过 PyPI 分发。
26
+ > **当前版本:0.2.12。**
27
+ > 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.12 通过 PyPI 分发。
28
28
  > **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
29
29
 
30
30
  ## 项目定位
@@ -305,6 +305,59 @@ class AdmissionFlowTests(unittest.TestCase):
305
305
  self.assertEqual(len(memories), 1)
306
306
  self.assertIn("交付延期风险", memories[0].memory.body)
307
307
 
308
+ def test_combined_orion_digest_is_dropped_but_atomic_action_is_kept(self):
309
+ service = self.make_service("orion-digest")
310
+ backend = QueueBackend()
311
+ user_key, _ = self.capture_turn(
312
+ service,
313
+ session="orion-digest",
314
+ turn="turn-1",
315
+ user="查看 Orion 邮件汇总并告诉我需要关注的事项。",
316
+ assistant="Orion汇总:子任务完成4条;现场增补待受理2条;另派发提供旧版本生产取数脚本。",
317
+ )
318
+ aggregate = candidate(
319
+ "orion-aggregate",
320
+ [user_key],
321
+ memory="Orion汇总(2026-09-02):子任务完成4条;现场增补待受理2条——另派发子任务提供旧版本生产取数脚本。",
322
+ type="fact",
323
+ )
324
+ aggregate["scopes"] = ["project:orion"]
325
+ atomic = candidate(
326
+ "orion-script-task",
327
+ [user_key],
328
+ memory="Orion需要提供旧版本生产取数脚本。",
329
+ type="todo",
330
+ )
331
+ atomic["scopes"] = ["project:orion"]
332
+ atomic_summary = json.loads(
333
+ summary(
334
+ user_key,
335
+ title="Orion提供旧版本生产取数脚本",
336
+ body="Orion需要提供旧版本生产取数脚本。",
337
+ type="todo",
338
+ )
339
+ )
340
+ atomic_summary.update(
341
+ {
342
+ "scopes": ["project:orion"],
343
+ "status": "active",
344
+ }
345
+ )
346
+ backend.responses.extend(
347
+ [gate([aggregate, atomic]), json.dumps(atomic_summary, ensure_ascii=False)]
348
+ )
349
+
350
+ result = service.process(source="hermes", session_id="orion-digest", model=backend)
351
+
352
+ self.assertEqual(result["memories_written"], 1)
353
+ self.assertEqual(result["deferred_candidates"], 0)
354
+ memories = service._read_memories_unlocked("knowledge")
355
+ self.assertEqual(len(memories), 1)
356
+ self.assertEqual(memories[0].memory.title, "Orion提供旧版本生产取数脚本")
357
+ self.assertNotIn("汇总", memories[0].memory.title)
358
+ self.assertNotIn("完成4条", memories[0].memory.body)
359
+ self.assertEqual([call["purpose"] for call in backend.calls], ["gate", "summarize"])
360
+
308
361
  def test_mixed_preference_is_kept_while_mcp_failure_is_not(self):
309
362
  service = self.make_service("mixed-preference")
310
363
  backend = QueueBackend()
@@ -284,7 +284,7 @@ class HermesProviderTests(unittest.TestCase):
284
284
 
285
285
  def test_provider_warns_with_one_line_update_when_core_versions_differ(self) -> None:
286
286
  client = FakeClient(responses=[{"stats": True}])
287
- client.server_version = "0.2.11"
287
+ client.server_version = "0.2.12"
288
288
  provider = provider_module.MemleafMemoryProvider()
289
289
  with patch.object(provider_module, "_resolve_command", return_value="memleaf-mcp"), \
290
290
  patch.object(provider_module, "_MCPClient", return_value=client), \
@@ -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.11"
306
+ client.server_version = "0.2.12"
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.11"), \
310
+ patch.object(provider_module, "_provider_manifest_version", return_value="0.2.12"), \
311
311
  patch.object(provider_module.logger, "warning") as warning:
312
312
  provider.initialize(
313
313
  "version-match-session",
@@ -324,7 +324,7 @@ class HermesProviderTests(unittest.TestCase):
324
324
  provider = provider_module.MemleafMemoryProvider()
325
325
  with patch.object(provider_module, "_resolve_command", return_value="memleaf-mcp"), \
326
326
  patch.object(provider_module, "_MCPClient", return_value=client), \
327
- patch.object(provider_module, "_provider_manifest_version", return_value="0.2.11"), \
327
+ patch.object(provider_module, "_provider_manifest_version", return_value="0.2.12"), \
328
328
  patch.object(provider_module.logger, "warning") as warning:
329
329
  provider.initialize(
330
330
  "version-missing-session",
@@ -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.11", (target / "plugin.yaml").read_text(encoding="utf-8"))
84
+ self.assertIn("version: 0.2.12", (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.11",
216
- "provider_version": "0.2.11",
215
+ "core_version": "0.2.12",
216
+ "provider_version": "0.2.12",
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.11", output)
224
- self.assertIn("Hermes provider=0.2.11", output)
223
+ self.assertIn("core=0.2.12", output)
224
+ self.assertIn("Hermes provider=0.2.12", 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"
@@ -23,7 +23,12 @@ from memleaf.prompts import (
23
23
  gate_prompt,
24
24
  summarize_prompt,
25
25
  )
26
- from memleaf.validation import ModelOutputError, NO_CHANGE_DECISION, normalize_relative_calendar_text
26
+ from memleaf.validation import (
27
+ ModelOutputError,
28
+ NO_CHANGE_DECISION,
29
+ is_aggregate_operational_text,
30
+ normalize_relative_calendar_text,
31
+ )
27
32
  from memleaf.index import event_key, turn_key
28
33
  from memleaf.llm import (
29
34
  CallableBackend,
@@ -398,6 +403,48 @@ class StageB1Test(unittest.TestCase):
398
403
  self.assertIsNone(normalize_relative_calendar_text("明天完成", None))
399
404
  self.assertIsNone(normalize_relative_calendar_text("明天完成", "invalid"))
400
405
 
406
+ def test_date_parentheses_are_removed_only_when_empty(self):
407
+ anchor = "2026-09-02T02:01:41Z"
408
+ self.assertEqual(
409
+ normalize_relative_calendar_text("2026-09-03()", anchor),
410
+ "2026-09-03",
411
+ )
412
+ self.assertEqual(
413
+ normalize_relative_calendar_text("2026-09-03()", anchor),
414
+ "2026-09-03",
415
+ )
416
+ summary = {
417
+ "title": "反馈截止 2026-09-03()",
418
+ "body": "English spelling: 2026-09-03()",
419
+ "tags": [],
420
+ "type": "todo",
421
+ "scopes": ["global"],
422
+ "sources": [{"event_key": "event-a"}],
423
+ "status": "active",
424
+ }
425
+ parsed = parse_summarize_output(json.dumps(summary), current_event_keys=["event-a"])
426
+ self.assertEqual(parsed["title"], "反馈截止 2026-09-03")
427
+ self.assertEqual(parsed["body"], "English spelling: 2026-09-03")
428
+ self.assertEqual(
429
+ parse_summarize_output(
430
+ json.dumps(dict(summary, body="2026-09-03(周四)")),
431
+ current_event_keys=["event-a"],
432
+ )["body"],
433
+ "2026-09-03(周四)",
434
+ )
435
+
436
+ def test_aggregate_operational_detector_requires_a_combined_digest(self):
437
+ self.assertTrue(
438
+ is_aggregate_operational_text(
439
+ "Orion汇总(2026-09-02):子任务完成4条;现场增补待受理2条——另派发1条。"
440
+ )
441
+ )
442
+ self.assertFalse(
443
+ is_aggregate_operational_text(
444
+ "Orion项目巡检:浦银安盛任务提供测试数据逾期5天。"
445
+ )
446
+ )
447
+
401
448
  def test_process_retries_schema_violation_until_third_gate_attempt_and_commits(self):
402
449
  responses = [
403
450
  json.dumps({"candidates": "invalid"}),
@@ -1255,6 +1255,193 @@ class StageB2ATest(unittest.TestCase):
1255
1255
  self.assertEqual(service.read(old.memory_id).body, "金元顺安实施计划新增部署要求。")
1256
1256
  self.assertEqual(len(service.vault.list_markdown("history")), 1)
1257
1257
 
1258
+ def test_same_project_plan_fact_candidate_is_reconciled_to_immutable_project_target(self):
1259
+ backend = QueueBackend()
1260
+ service = self.service(backend, name="inferred-plan-update")
1261
+ old = service.create_memory(
1262
+ memory_id="mem-zhongyin-plan",
1263
+ title="中银国际员工投资行为申报系统信创改造实施计划",
1264
+ body="中银国际实施计划当前按原始安排执行。",
1265
+ type="project",
1266
+ scopes=["project:中银国际"],
1267
+ )
1268
+ user_key, assistant_key = self.capture_turn(
1269
+ service,
1270
+ turn="inferred-plan-update",
1271
+ user_event="inferred-plan-update-user",
1272
+ assistant_event="inferred-plan-update-assistant",
1273
+ user="中银国际客户提出实施计划调整建议。",
1274
+ assistant="已记录中银国际实施计划调整建议,待更新计划。",
1275
+ )
1276
+ candidate = self.candidate(
1277
+ "zhongyin-plan-feedback",
1278
+ [user_key, assistant_key],
1279
+ memory="中银国际客户提出实施计划调整建议:提前部署测试环境并重新压实计划。",
1280
+ type="fact",
1281
+ )
1282
+ candidate["scopes"] = ["project:中银国际"]
1283
+ backend.responses.extend(
1284
+ [
1285
+ self.gate([candidate]),
1286
+ self.summary(
1287
+ user_key,
1288
+ title=old.title,
1289
+ body="中银国际实施计划当前按原始安排执行;客户提出提前部署测试环境并重新压实计划。",
1290
+ type="project",
1291
+ scopes=["project:中银国际"],
1292
+ ),
1293
+ ]
1294
+ )
1295
+
1296
+ result = service.process(source="codex", session_id="s", model=backend)
1297
+
1298
+ self.assertEqual(result["memory_ids"], [old.memory_id])
1299
+ self.assertEqual(len(self.knowledge(service)), 1)
1300
+ self.assertEqual(service.read(old.memory_id).type, "project")
1301
+ self.assertIn("原始安排", service.read(old.memory_id).body)
1302
+ self.assertIn("提前部署测试环境", service.read(old.memory_id).body)
1303
+ self.assertEqual(len(service.vault.list_markdown("history")), 1)
1304
+ self.assertNotIn("update_target_type_mismatch", " ".join(call["prompt"] for call in backend.calls))
1305
+
1306
+ def test_project_plan_target_wins_over_sent_mail_for_zhongyin_and_jinyuan(self):
1307
+ cases = (
1308
+ (
1309
+ "zhongyin",
1310
+ "中银国际",
1311
+ "中银国际员工申报系统信创改造实施计划",
1312
+ "陈国金",
1313
+ ),
1314
+ (
1315
+ "jinyuan",
1316
+ "金元顺安",
1317
+ "金元顺安员工投资行为申报系统信创改造实施计划",
1318
+ "方明",
1319
+ ),
1320
+ )
1321
+ for slug, project, plan_title, recipient in cases:
1322
+ with self.subTest(project=project):
1323
+ backend = QueueBackend()
1324
+ service = self.service(backend, name=f"multi-target-{slug}")
1325
+ plan = service.create_memory(
1326
+ memory_id=f"mem-{slug}-plan",
1327
+ title=plan_title,
1328
+ body=f"{project}实施计划当前按原始安排执行。",
1329
+ type="project",
1330
+ scopes=[f"project:{project}"],
1331
+ )
1332
+ adjacent = []
1333
+ for suffix, title, body in (
1334
+ (
1335
+ "sent-mail",
1336
+ f"已发送{plan_title}邮件给{recipient}",
1337
+ f"已向{recipient}发送{project}实施计划邮件。",
1338
+ ),
1339
+ (
1340
+ "attachment",
1341
+ f"{project}实施计划附件清单",
1342
+ f"{project}实施计划附件已归档。",
1343
+ ),
1344
+ (
1345
+ "meeting",
1346
+ f"{project}实施计划会议纪要",
1347
+ f"{project}实施计划会议已完成。",
1348
+ ),
1349
+ ):
1350
+ adjacent.append(
1351
+ service.create_memory(
1352
+ memory_id=f"mem-{slug}-{suffix}",
1353
+ title=title,
1354
+ body=body,
1355
+ type="fact",
1356
+ scopes=[f"project:{project}"],
1357
+ )
1358
+ )
1359
+ user_key, assistant_key = self.capture_turn(
1360
+ service,
1361
+ turn=f"multi-target-{slug}",
1362
+ user_event=f"multi-target-{slug}-user",
1363
+ assistant_event=f"multi-target-{slug}-assistant",
1364
+ user=f"{project}客户提出实施计划调整建议。",
1365
+ assistant=f"已记录{project}实施计划的新约束,待更新计划。",
1366
+ )
1367
+ candidate = self.candidate(
1368
+ f"{slug}-plan-feedback",
1369
+ [user_key, assistant_key],
1370
+ memory=f"{project}客户提出实施计划调整建议:提前部署测试环境并重新压实计划。",
1371
+ type="fact",
1372
+ )
1373
+ candidate["scopes"] = [f"project:{project}"]
1374
+ backend.responses.extend(
1375
+ [
1376
+ self.gate([candidate]),
1377
+ self.summary(
1378
+ user_key,
1379
+ title=plan_title,
1380
+ body=(
1381
+ f"{project}实施计划当前按原始安排执行;"
1382
+ "客户提出提前部署测试环境并重新压实计划。"
1383
+ ),
1384
+ type="project",
1385
+ scopes=[f"project:{project}"],
1386
+ ),
1387
+ ]
1388
+ )
1389
+
1390
+ result = service.process(source="codex", session_id="s", model=backend)
1391
+
1392
+ self.assertEqual(result["memory_ids"], [plan.memory_id])
1393
+ self.assertEqual([call["purpose"] for call in backend.calls], ["gate", "summarize"])
1394
+ self.assertEqual(len(self.knowledge(service)), 4)
1395
+ self.assertIn("提前部署测试环境", service.read(plan.memory_id).body)
1396
+ for record in adjacent:
1397
+ self.assertEqual(service.read(record.memory_id).body, record.body)
1398
+ self.assertEqual(len(service.vault.list_markdown("history")), 1)
1399
+
1400
+ def test_multiple_same_project_plan_targets_are_deferred_without_sibling(self):
1401
+ backend = QueueBackend()
1402
+ service = self.service(backend, name="ambiguous-project-plan-target")
1403
+ for memory_id, title in (
1404
+ ("mem-jinyuan-plan-test", "金元顺安实施计划:测试环境"),
1405
+ ("mem-jinyuan-plan-release", "金元顺安实施计划:上线安排"),
1406
+ ):
1407
+ service.create_memory(
1408
+ memory_id=memory_id,
1409
+ title=title,
1410
+ body=f"{title}按原始安排执行。",
1411
+ type="project",
1412
+ scopes=["project:金元顺安"],
1413
+ )
1414
+ user_key, assistant_key = self.capture_turn(
1415
+ service,
1416
+ turn="ambiguous-project-plan-target",
1417
+ user_event="ambiguous-project-plan-target-user",
1418
+ assistant_event="ambiguous-project-plan-target-assistant",
1419
+ user="金元顺安客户提出实施计划调整建议。",
1420
+ assistant="已记录金元顺安实施计划的新约束,待确认对应计划。",
1421
+ )
1422
+ candidate = self.candidate(
1423
+ "ambiguous-jinyuan-plan-feedback",
1424
+ [user_key, assistant_key],
1425
+ memory="金元顺安实施计划",
1426
+ type="fact",
1427
+ )
1428
+ candidate["scopes"] = ["project:金元顺安"]
1429
+ backend.responses.append(self.gate([candidate]))
1430
+
1431
+ result = service.process(source="codex", session_id="s", model=backend)
1432
+
1433
+ self.assertEqual(result["memory_ids"], [])
1434
+ self.assertEqual(result["memories_written"], 0)
1435
+ self.assertEqual(result["deferred_candidates"], 1)
1436
+ self.assertEqual([call["purpose"] for call in backend.calls], ["gate"])
1437
+ self.assertEqual(
1438
+ self.processed(service)["sessions"]["codex/s"]["processed_turns"][0]["deferred_candidates"][0]["reason"],
1439
+ "ambiguous_update_target",
1440
+ )
1441
+ self.assertEqual(len(self.knowledge(service)), 2)
1442
+ self.assertEqual(len(service.vault.list_markdown("history")), 0)
1443
+ self.assertTrue((service.vault.inbox_path / "codex" / "s.md").is_file())
1444
+
1258
1445
  def test_same_project_wrong_update_target_final_retry_becomes_create(self):
1259
1446
  backend = QueueBackend()
1260
1447
  service = self.service(backend, name="same-project-wrong-target-final-retry")
@@ -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.11"},
107
+ {"name": "memleaf", "version": "0.2.12"},
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.11")
27
+ self.assertEqual(self.project["version"], "0.2.12")
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