memleaf 0.2.13__tar.gz → 0.2.14__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.13 → memleaf-0.2.14}/CHANGELOG.md +11 -0
- {memleaf-0.2.13/src/memleaf.egg-info → memleaf-0.2.14}/PKG-INFO +3 -3
- {memleaf-0.2.13 → memleaf-0.2.14}/README.en.md +2 -2
- {memleaf-0.2.13 → memleaf-0.2.14}/README.md +2 -2
- {memleaf-0.2.13 → memleaf-0.2.14}/pyproject.toml +1 -1
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/hermes_provider/__init__.py +1 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/processing.py +13 -20
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/prompts.py +39 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/validation.py +265 -0
- {memleaf-0.2.13 → memleaf-0.2.14/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/SOURCES.txt +1 -0
- memleaf-0.2.14/tests/test_extraction_quality_regressions.py +680 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_hermes_provider.py +42 -4
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_pypi_install.py +5 -5
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b2b.py +1 -1
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.13 → memleaf-0.2.14}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/LICENSE +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/MANIFEST.in +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/examples/README.md +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/examples/basic_usage.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/install.ps1 +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/install.sh +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/setup.cfg +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/config.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/index.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/models.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/service.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/__init__.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_admission_noise.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_host_events.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_install.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b1.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b2a.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.13 → memleaf-0.2.14}/tests/test_v2_search_gate_acceptance.py +0 -0
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to memleaf are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.14 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Reject a permanent-memory candidate that combines a durable project rule or
|
|
8
|
+
implementation-plan change with an independent dated todo, while preserving
|
|
9
|
+
multiple dated milestones that belong to one project plan.
|
|
10
|
+
- Normalize explicit implementation plans and plan adjustments to `project`
|
|
11
|
+
without promoting adjacent mail, attachment, meeting, or archive records.
|
|
12
|
+
- Keep the gate-selected type immutable through summarization for both CREATE
|
|
13
|
+
and UPDATE, preserve multiple legal update candidates, and expose the new
|
|
14
|
+
`mixed_future_use` diagnostic through the Hermes provider.
|
|
15
|
+
|
|
5
16
|
## 0.2.13 — 2026-09-02
|
|
6
17
|
|
|
7
18
|
- Preserve the full active project-plan body, stable title, and retrieval
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
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.14。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.14 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[中文](README.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **Version: 0.2.
|
|
8
|
-
> The core library, Vault, stdio MCP server, initialization CLI, model routing, memory extraction, controlled retrieval protocol, and host adapters are implemented. memleaf 0.2.
|
|
7
|
+
> **Version: 0.2.14.**
|
|
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.14 is distributed through PyPI.
|
|
9
9
|
> **The current release supports Hermes and Codex.** Antigravity is not detected, installed, or configured.
|
|
10
10
|
|
|
11
11
|
## Project scope
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
[English](README.en.md) · [PyPI](https://pypi.org/project/memleaf/) · [GitHub](https://github.com/miffyblueboo/memleaf)
|
|
6
6
|
|
|
7
|
-
> **当前版本:0.2.
|
|
8
|
-
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.
|
|
7
|
+
> **当前版本:0.2.14。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.14 通过 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.14"
|
|
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
|
|
@@ -34,6 +34,7 @@ from .prompts import (
|
|
|
34
34
|
DUPLICATE_TARGET_CORRECTION,
|
|
35
35
|
GATE_TYPE_CORRECTION,
|
|
36
36
|
JSON_CORRECTION,
|
|
37
|
+
MIXED_FUTURE_USE_CORRECTION,
|
|
37
38
|
MIXED_PROJECT_SCOPES_CORRECTION,
|
|
38
39
|
RELATIVE_TIME_CORRECTION,
|
|
39
40
|
GATE_SYSTEM,
|
|
@@ -66,6 +67,7 @@ from .validation import (
|
|
|
66
67
|
normalize_relative_calendar_text,
|
|
67
68
|
is_aggregate_operational_text,
|
|
68
69
|
is_attachment_followup_only_text,
|
|
70
|
+
is_project_plan_text,
|
|
69
71
|
)
|
|
70
72
|
from .vault import safe_component
|
|
71
73
|
|
|
@@ -599,6 +601,8 @@ class Processor:
|
|
|
599
601
|
return DUPLICATE_TARGET_CORRECTION
|
|
600
602
|
if hint == "mixed_project_scopes":
|
|
601
603
|
return MIXED_PROJECT_SCOPES_CORRECTION
|
|
604
|
+
if hint == "mixed_future_use":
|
|
605
|
+
return MIXED_FUTURE_USE_CORRECTION
|
|
602
606
|
if stage == "gate" and hint == "update_target_type_mismatch":
|
|
603
607
|
return UPDATE_TARGET_TYPE_CORRECTION
|
|
604
608
|
if stage == "gate" and hint == "invalid_type":
|
|
@@ -1696,22 +1700,18 @@ class Processor:
|
|
|
1696
1700
|
|
|
1697
1701
|
@staticmethod
|
|
1698
1702
|
def _is_project_plan_title(value: Any) -> bool:
|
|
1699
|
-
|
|
1700
|
-
return any(
|
|
1701
|
-
marker in text
|
|
1702
|
-
for marker in ("实施计划", "项目计划", "implementation plan", "project plan")
|
|
1703
|
-
)
|
|
1703
|
+
return is_project_plan_text(value)
|
|
1704
1704
|
|
|
1705
1705
|
@staticmethod
|
|
1706
1706
|
def _is_adjacent_plan_record(value: Any) -> bool:
|
|
1707
1707
|
text = normalize_term(value) if isinstance(value, str) else ""
|
|
1708
|
-
return any(
|
|
1708
|
+
return bool(text) and any(
|
|
1709
1709
|
marker in text
|
|
1710
1710
|
for marker in (
|
|
1711
1711
|
"已发送", "发送", "邮件", "附件", "存档", "会议", "启动会", "纪要",
|
|
1712
1712
|
"sent", "email", "mail", "attachment", "archive", "meeting", "minutes",
|
|
1713
1713
|
)
|
|
1714
|
-
)
|
|
1714
|
+
) and not is_project_plan_text(value)
|
|
1715
1715
|
|
|
1716
1716
|
@classmethod
|
|
1717
1717
|
def _merge_additive_project_plan_update(
|
|
@@ -2335,20 +2335,13 @@ class Processor:
|
|
|
2335
2335
|
expected_scopes=candidate["scopes"],
|
|
2336
2336
|
expected_scope_source=candidate["scope_source"],
|
|
2337
2337
|
allow_no_change=True,
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
)
|
|
2344
|
-
else None
|
|
2345
|
-
),
|
|
2338
|
+
# The summarize stage may not reinterpret a gate
|
|
2339
|
+
# candidate, including CREATE candidates. Updates
|
|
2340
|
+
# additionally retain the active target's immutable
|
|
2341
|
+
# type below.
|
|
2342
|
+
expected_type=candidate.get("type"),
|
|
2346
2343
|
expected_update_memory_id=gate_update_target,
|
|
2347
|
-
expected_target_type=
|
|
2348
|
-
gate_target_type
|
|
2349
|
-
if gate_target_type == candidate.get("type")
|
|
2350
|
-
else None
|
|
2351
|
-
),
|
|
2344
|
+
expected_target_type=gate_target_type,
|
|
2352
2345
|
),
|
|
2353
2346
|
diagnostic_context={
|
|
2354
2347
|
"source": turn.source,
|
|
@@ -108,6 +108,13 @@ be independently retrieved and updated. Prefer one coherent current-state memory
|
|
|
108
108
|
over adjacent overlapping snapshots; do not use a score or fixed category rule.
|
|
109
109
|
Most ordinary turns should produce zero or one candidate; multiple require
|
|
110
110
|
genuinely independent future questions or actions.
|
|
111
|
+
Never combine a durable project rule or implementation-plan change with an
|
|
112
|
+
independent dated todo in one candidate. For example, a project plan/rule and
|
|
113
|
+
"reply or investigate by YYYY-MM-DD" are two future uses: emit separate
|
|
114
|
+
candidates, preserving each candidate's own evidence and project scope. A
|
|
115
|
+
date that is an intrinsic milestone of the same implementation plan may remain
|
|
116
|
+
inside that one project candidate. A candidate that combines these independent
|
|
117
|
+
uses is invalid and must be split before summarization.
|
|
111
118
|
Never persist an aggregate mailbox sweep, daily report, or "items to watch"
|
|
112
119
|
digest as one cross-project memory. Inspect each item and split genuinely
|
|
113
120
|
independent project risks or actionable todo items; if an item has no
|
|
@@ -155,6 +162,16 @@ same-use project targets remain, do not guess an update target. A candidate
|
|
|
155
162
|
that still combines a mailbox or daily digest shell with counts/statuses is not
|
|
156
163
|
atomic: set worth=false for the aggregate, and emit any independent concrete
|
|
157
164
|
action as a separate candidate.
|
|
165
|
+
If the same confirmed change applies to multiple same-project active memories,
|
|
166
|
+
emit one candidate per independently changed memory and set each exact
|
|
167
|
+
update_memory_id; never select one target and silently omit the others. Do not
|
|
168
|
+
target adjacent sent-mail, attachment, meeting, or archive records unless the
|
|
169
|
+
current evidence changes their own future-use topic.
|
|
170
|
+
An explicit implementation plan, project implementation plan, or plan
|
|
171
|
+
adjustment is type project. Do not label it fact merely because it came from
|
|
172
|
+
an email or because no existing project memory was retrieved. Keep an actual
|
|
173
|
+
todo as todo only when its future use is the action itself, not the durable
|
|
174
|
+
plan it mentions.
|
|
158
175
|
Return no prose, markdown fences, comments, or trailing text."""
|
|
159
176
|
|
|
160
177
|
|
|
@@ -265,6 +282,16 @@ plan target over same-topic sent-mail, attachment/archive, or meeting records;
|
|
|
265
282
|
the target type remains immutable. Never summarize a combined mailbox/daily
|
|
266
283
|
digest shell with counts or statuses; return NO_CHANGE for that aggregate,
|
|
267
284
|
while a separate atomic future-use action may still be summarized.
|
|
285
|
+
If the current confirmed change applies to multiple same-project active
|
|
286
|
+
memories, the gate will provide separate candidates and exact update targets;
|
|
287
|
+
preserve each candidate's target and do not collapse them into one summary.
|
|
288
|
+
Never combine a durable project rule or implementation-plan change with an
|
|
289
|
+
independent dated todo in one summary. A plan/rule and a dated reply,
|
|
290
|
+
investigation, submission, or follow-up are separate future uses; such output
|
|
291
|
+
is invalid and must be retried as separate gate candidates. A date that is an
|
|
292
|
+
intrinsic milestone of the same project plan may remain in its project summary.
|
|
293
|
+
An explicit implementation plan or plan adjustment must use type project;
|
|
294
|
+
preserve an existing update target's type when it is the same project plan.
|
|
268
295
|
Return no prose, markdown fences, comments, or trailing text."""
|
|
269
296
|
|
|
270
297
|
|
|
@@ -325,6 +352,18 @@ MIXED_PROJECT_SCOPES_CORRECTION = (
|
|
|
325
352
|
)
|
|
326
353
|
|
|
327
354
|
|
|
355
|
+
MIXED_FUTURE_USE_CORRECTION = (
|
|
356
|
+
"Previous output violated: mixed_future_use. Keep every candidate and "
|
|
357
|
+
"summary atomic. Split a durable project rule or implementation-plan "
|
|
358
|
+
"change from an independent dated todo such as a reply, investigation, "
|
|
359
|
+
"submission, or follow-up; preserve each part's own evidence and project "
|
|
360
|
+
"scope. A date intrinsic to the same implementation plan may remain in "
|
|
361
|
+
"that project candidate. Return separate gate candidates or return "
|
|
362
|
+
"NO_CHANGE for a part with no independent future use. Return only the "
|
|
363
|
+
"strict JSON object."
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
328
367
|
DUPLICATE_TARGET_CORRECTION = (
|
|
329
368
|
"Previous output violated: duplicate_update_target. Merge every candidate "
|
|
330
369
|
"that refers to the same active memory target into one candidate for one "
|
|
@@ -75,6 +75,7 @@ MODEL_VALIDATION_DETAILS = frozenset(
|
|
|
75
75
|
"source_shape",
|
|
76
76
|
"todo_fields",
|
|
77
77
|
"relative_time",
|
|
78
|
+
"mixed_future_use",
|
|
78
79
|
"other_schema_violation",
|
|
79
80
|
)
|
|
80
81
|
)
|
|
@@ -377,6 +378,229 @@ _AGGREGATE_COUNT = re.compile(
|
|
|
377
378
|
)
|
|
378
379
|
|
|
379
380
|
|
|
381
|
+
# These are intentionally narrow, high-signal markers. The gate is still
|
|
382
|
+
# responsible for deciding whether a candidate is worth retaining; these
|
|
383
|
+
# helpers only protect the write boundary from a known class of semantic
|
|
384
|
+
# contracts that cannot safely be represented by one memory.
|
|
385
|
+
_PROJECT_PLAN_MARKERS = (
|
|
386
|
+
"实施计划",
|
|
387
|
+
"项目计划",
|
|
388
|
+
"实施方案",
|
|
389
|
+
"项目方案",
|
|
390
|
+
"计划调整",
|
|
391
|
+
"调整计划",
|
|
392
|
+
"调整实施计划",
|
|
393
|
+
"重新压实计划",
|
|
394
|
+
"重新压实实施计划",
|
|
395
|
+
"重新制定计划",
|
|
396
|
+
"部署计划",
|
|
397
|
+
"上线计划",
|
|
398
|
+
"implementation plan",
|
|
399
|
+
"project plan",
|
|
400
|
+
"implementation schedule",
|
|
401
|
+
"plan adjustment",
|
|
402
|
+
"adjust the plan",
|
|
403
|
+
"revise the plan",
|
|
404
|
+
"rebaseline the plan",
|
|
405
|
+
)
|
|
406
|
+
_ADJACENT_PLAN_RECORD_MARKERS = (
|
|
407
|
+
"邮件",
|
|
408
|
+
"附件",
|
|
409
|
+
"会议纪要",
|
|
410
|
+
"会议记录",
|
|
411
|
+
"启动会",
|
|
412
|
+
"存档",
|
|
413
|
+
"归档",
|
|
414
|
+
"email",
|
|
415
|
+
"mail",
|
|
416
|
+
"attachment",
|
|
417
|
+
"meeting minutes",
|
|
418
|
+
"meeting record",
|
|
419
|
+
"archive",
|
|
420
|
+
)
|
|
421
|
+
_PROJECT_RULE_MARKERS = (
|
|
422
|
+
"项目要求",
|
|
423
|
+
"项目约束",
|
|
424
|
+
"交付要求",
|
|
425
|
+
"技术路线",
|
|
426
|
+
"长期要求",
|
|
427
|
+
"长期约束",
|
|
428
|
+
"统一规范",
|
|
429
|
+
"固定规则",
|
|
430
|
+
"以后",
|
|
431
|
+
"project requirement",
|
|
432
|
+
"project constraint",
|
|
433
|
+
"delivery requirement",
|
|
434
|
+
"technical approach",
|
|
435
|
+
)
|
|
436
|
+
_DATED_TODO_MARKERS = (
|
|
437
|
+
"待办",
|
|
438
|
+
"排查",
|
|
439
|
+
"答复",
|
|
440
|
+
"回复",
|
|
441
|
+
"提交",
|
|
442
|
+
"跟进",
|
|
443
|
+
"提醒",
|
|
444
|
+
"联系",
|
|
445
|
+
"发给",
|
|
446
|
+
"发送",
|
|
447
|
+
"处理",
|
|
448
|
+
"确认",
|
|
449
|
+
"整改",
|
|
450
|
+
"修复",
|
|
451
|
+
"回访",
|
|
452
|
+
"催办",
|
|
453
|
+
"反馈",
|
|
454
|
+
"完成",
|
|
455
|
+
"补充材料",
|
|
456
|
+
"提供",
|
|
457
|
+
"todo",
|
|
458
|
+
"deadline",
|
|
459
|
+
"due",
|
|
460
|
+
"follow up",
|
|
461
|
+
"follow-up",
|
|
462
|
+
)
|
|
463
|
+
_INDEPENDENT_TODO_MARKERS = tuple(
|
|
464
|
+
marker for marker in _DATED_TODO_MARKERS if marker != "完成"
|
|
465
|
+
)
|
|
466
|
+
_PLAN_MILESTONE_MARKERS = (
|
|
467
|
+
"计划",
|
|
468
|
+
"上线日期",
|
|
469
|
+
"交付日期",
|
|
470
|
+
"完成日期",
|
|
471
|
+
"工期",
|
|
472
|
+
"工作日",
|
|
473
|
+
"里程碑",
|
|
474
|
+
"上线",
|
|
475
|
+
"验收",
|
|
476
|
+
"交付",
|
|
477
|
+
"迁移",
|
|
478
|
+
"milestone",
|
|
479
|
+
)
|
|
480
|
+
_PLAN_STAGE_TEXT = re.compile(
|
|
481
|
+
r"(?:第[一二三四五六七八九十百\d]+阶段|阶段[一二三四五六七八九十百\d]+|"
|
|
482
|
+
r"phase\s*(?:one|two|three|four|five|\d+))",
|
|
483
|
+
re.IGNORECASE,
|
|
484
|
+
)
|
|
485
|
+
_CALENDAR_TEXT = re.compile(
|
|
486
|
+
rf"(?:{_RELATIVE_DATE_TOKEN}|"
|
|
487
|
+
r"(?<![A-Za-z\d./-])(?:\d{4}[/\-](?:0?[1-9]|1[0-2])[/\-](?:0?[1-9]|[12]\d|3[01])|"
|
|
488
|
+
r"(?:\d{4}\s*年\s*)?(?:0?[1-9]|1[0-2])\s*月\s*(?:0?[1-9]|[12]\d|3[01])\s*日?)"
|
|
489
|
+
r")",
|
|
490
|
+
re.IGNORECASE,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def is_project_plan_text(value: Any) -> bool:
|
|
495
|
+
"""Return whether text explicitly describes a project plan or adjustment."""
|
|
496
|
+
|
|
497
|
+
if not isinstance(value, str):
|
|
498
|
+
return False
|
|
499
|
+
folded = value.casefold()
|
|
500
|
+
# Only a record whose own label/phrase is a plan mail, attachment, meeting
|
|
501
|
+
# record, or archive is adjacent. A durable plan may still mention that
|
|
502
|
+
# an attachment must be migrated, an email sent, or a meeting held.
|
|
503
|
+
compact = re.sub(r"[\s::,,;;。.!!??()()\[\]【】_-]+", "", folded)
|
|
504
|
+
plan_markers = tuple(
|
|
505
|
+
re.sub(r"\s+", "", marker.casefold()) for marker in _PROJECT_PLAN_MARKERS
|
|
506
|
+
)
|
|
507
|
+
adjacent_markers = tuple(
|
|
508
|
+
re.sub(r"\s+", "", marker.casefold())
|
|
509
|
+
for marker in _ADJACENT_PLAN_RECORD_MARKERS
|
|
510
|
+
)
|
|
511
|
+
direct_adjacent = any(
|
|
512
|
+
f"{plan}{adjacent}" in compact or f"{adjacent}{plan}" in compact
|
|
513
|
+
for plan in plan_markers
|
|
514
|
+
for adjacent in adjacent_markers
|
|
515
|
+
)
|
|
516
|
+
archived_plan = any(
|
|
517
|
+
f"{plan}已{archive}" in compact or f"已{archive}{plan}" in compact
|
|
518
|
+
for plan in plan_markers
|
|
519
|
+
for archive in ("存档", "归档", "archive")
|
|
520
|
+
)
|
|
521
|
+
sent_plan_mail = bool(
|
|
522
|
+
re.search(
|
|
523
|
+
r"(?:已发送|发送|sent|sentto).{0,12}"
|
|
524
|
+
r"(?:实施计划|项目计划|实施方案|项目方案|部署计划|上线计划|"
|
|
525
|
+
r"implementationplan|projectplan|implementationschedule)"
|
|
526
|
+
r"(?:邮件|email|mail)",
|
|
527
|
+
compact,
|
|
528
|
+
re.IGNORECASE,
|
|
529
|
+
)
|
|
530
|
+
)
|
|
531
|
+
if direct_adjacent or archived_plan or sent_plan_mail:
|
|
532
|
+
return False
|
|
533
|
+
return any(marker in folded for marker in _PROJECT_PLAN_MARKERS)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _split_future_use_clauses(value: str) -> list[str]:
|
|
537
|
+
# Semicolons, sentence boundaries, and explicit contrast/conjunction
|
|
538
|
+
# markers are the only boundaries used here. Commas are deliberately not
|
|
539
|
+
# boundaries because implementation plans commonly enumerate milestones.
|
|
540
|
+
return [
|
|
541
|
+
part.strip()
|
|
542
|
+
for part in re.split(
|
|
543
|
+
r"[;;。!?!?\n]+|(?:同时|另外|此外|其次|并且|并要求|还需|另需|in addition|separately)",
|
|
544
|
+
value,
|
|
545
|
+
flags=re.IGNORECASE,
|
|
546
|
+
)
|
|
547
|
+
if part.strip()
|
|
548
|
+
]
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def is_mixed_future_use_text(value: Any) -> bool:
|
|
552
|
+
"""Detect a plan/rule combined with an independent dated todo.
|
|
553
|
+
|
|
554
|
+
This is a safety boundary, not a general-purpose classifier. It only
|
|
555
|
+
returns true when the durable plan/rule and a strong dated action occur in
|
|
556
|
+
separate clauses. A plan's own milestone or delivery date therefore
|
|
557
|
+
remains representable as one project memory.
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
if not isinstance(value, str):
|
|
561
|
+
return False
|
|
562
|
+
clauses = _split_future_use_clauses(value)
|
|
563
|
+
if len(clauses) < 2:
|
|
564
|
+
return False
|
|
565
|
+
durable_indexes = {
|
|
566
|
+
index
|
|
567
|
+
for index, clause in enumerate(clauses)
|
|
568
|
+
if is_project_plan_text(clause)
|
|
569
|
+
or any(marker in clause.casefold() for marker in _PROJECT_RULE_MARKERS)
|
|
570
|
+
}
|
|
571
|
+
if not durable_indexes:
|
|
572
|
+
return False
|
|
573
|
+
plan_milestone_context = any(
|
|
574
|
+
any(marker in clauses[index].casefold() for marker in ("里程碑", "milestone"))
|
|
575
|
+
for index in durable_indexes
|
|
576
|
+
)
|
|
577
|
+
plan_stage_context = sum(bool(_PLAN_STAGE_TEXT.search(clause)) for clause in clauses) >= 2
|
|
578
|
+
todo_indexes = {
|
|
579
|
+
index
|
|
580
|
+
for index, clause in enumerate(clauses)
|
|
581
|
+
if _CALENDAR_TEXT.search(clause)
|
|
582
|
+
and any(marker in clause.casefold() for marker in _DATED_TODO_MARKERS)
|
|
583
|
+
and not (
|
|
584
|
+
"完成" in clause
|
|
585
|
+
and any(marker in clause.casefold() for marker in _PLAN_MILESTONE_MARKERS)
|
|
586
|
+
and not any(
|
|
587
|
+
marker in clause.casefold() for marker in _INDEPENDENT_TODO_MARKERS
|
|
588
|
+
)
|
|
589
|
+
)
|
|
590
|
+
and not (
|
|
591
|
+
(plan_milestone_context or plan_stage_context)
|
|
592
|
+
and "完成" in clause
|
|
593
|
+
and not any(
|
|
594
|
+
marker in clause.casefold() for marker in _INDEPENDENT_TODO_MARKERS
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
# A second clause that is itself the project plan is a plan milestone,
|
|
598
|
+
# not evidence of a second future-use object.
|
|
599
|
+
and not is_project_plan_text(clause)
|
|
600
|
+
}
|
|
601
|
+
return bool(todo_indexes - durable_indexes)
|
|
602
|
+
|
|
603
|
+
|
|
380
604
|
def is_aggregate_operational_text(value: Any) -> bool:
|
|
381
605
|
"""Recognize a combined operational digest, not one concrete work item.
|
|
382
606
|
|
|
@@ -910,6 +1134,23 @@ def validate_gate_output(
|
|
|
910
1134
|
raise ModelOutputError("duplicate candidate cannot be worth remembering", validation_detail="invalid_flags")
|
|
911
1135
|
if item["worth"] and candidate_type is None:
|
|
912
1136
|
raise ModelOutputError("worth candidate must have a type", validation_detail="invalid_type")
|
|
1137
|
+
if item["worth"] and is_mixed_future_use_text(item["memory"]):
|
|
1138
|
+
raise ModelOutputError(
|
|
1139
|
+
"one candidate combines independent future uses",
|
|
1140
|
+
validation_detail="mixed_future_use",
|
|
1141
|
+
)
|
|
1142
|
+
# A clearly named implementation plan/plan adjustment is a project
|
|
1143
|
+
# memory, even when the model labels it as a generic fact. Keep todo
|
|
1144
|
+
# candidates intact: a task list that merely mentions a plan is still
|
|
1145
|
+
# an actionable todo, while fact/event/other are the common accidental
|
|
1146
|
+
# labels observed for durable plan changes.
|
|
1147
|
+
if (
|
|
1148
|
+
item["worth"]
|
|
1149
|
+
and candidate_type in {"fact", "event", "other"}
|
|
1150
|
+
and is_project_plan_text(item["memory"])
|
|
1151
|
+
):
|
|
1152
|
+
item["type"] = "project"
|
|
1153
|
+
candidate_type = "project"
|
|
913
1154
|
if "update_memory_id" in item and "duplicate_memory_id" in item:
|
|
914
1155
|
raise ModelOutputError(
|
|
915
1156
|
"a candidate cannot set both duplicate_memory_id and update_memory_id",
|
|
@@ -1117,10 +1358,32 @@ def validate_summarize_output(
|
|
|
1117
1358
|
_string(item["title"], "title")
|
|
1118
1359
|
_string(item["body"], "body", multiline=True)
|
|
1119
1360
|
_reject_relative_calendar_expression(item)
|
|
1361
|
+
if is_mixed_future_use_text(f"{item['title']}\n{item['body']}"):
|
|
1362
|
+
raise ModelOutputError(
|
|
1363
|
+
"one summary combines independent future uses",
|
|
1364
|
+
validation_detail="mixed_future_use",
|
|
1365
|
+
)
|
|
1120
1366
|
item["tags"] = _string_list(item["tags"], "tags")
|
|
1121
1367
|
candidate_type = item["type"]
|
|
1122
1368
|
if not isinstance(candidate_type, str) or candidate_type not in MEMORY_TYPES:
|
|
1123
1369
|
raise ModelOutputError("invalid memory type", validation_detail="invalid_type")
|
|
1370
|
+
inferred_project_type = (
|
|
1371
|
+
candidate_type in {"fact", "event", "other"}
|
|
1372
|
+
and is_project_plan_text(f"{item['title']}\n{item['body']}")
|
|
1373
|
+
)
|
|
1374
|
+
if inferred_project_type:
|
|
1375
|
+
if expected_type is not None and expected_type != "project":
|
|
1376
|
+
raise ModelOutputError(
|
|
1377
|
+
"project plan summary cannot use a non-project gate type",
|
|
1378
|
+
validation_detail="invalid_type",
|
|
1379
|
+
)
|
|
1380
|
+
if expected_target_type is not None and expected_target_type != "project":
|
|
1381
|
+
raise ModelOutputError(
|
|
1382
|
+
"project plan summary cannot update a non-project target",
|
|
1383
|
+
validation_detail="invalid_type",
|
|
1384
|
+
)
|
|
1385
|
+
candidate_type = "project"
|
|
1386
|
+
item["type"] = candidate_type
|
|
1124
1387
|
if expected_type is not None and candidate_type != expected_type:
|
|
1125
1388
|
raise ModelOutputError(
|
|
1126
1389
|
"summary type differs from gate candidate",
|
|
@@ -1375,6 +1638,8 @@ __all__ = [
|
|
|
1375
1638
|
"parse_strict_json",
|
|
1376
1639
|
"is_aggregate_operational_text",
|
|
1377
1640
|
"is_attachment_followup_only_text",
|
|
1641
|
+
"is_project_plan_text",
|
|
1642
|
+
"is_mixed_future_use_text",
|
|
1378
1643
|
"normalize_relative_calendar_text",
|
|
1379
1644
|
"parse_summarize_output",
|
|
1380
1645
|
"parse_summary_output",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memleaf
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.14
|
|
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.14。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.14 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|
|
@@ -67,6 +67,7 @@ tests/test_codex_native_cli.py
|
|
|
67
67
|
tests/test_context_budget.py
|
|
68
68
|
tests/test_credential_safety.py
|
|
69
69
|
tests/test_cross_host_acceptance.py
|
|
70
|
+
tests/test_extraction_quality_regressions.py
|
|
70
71
|
tests/test_hermes_provider.py
|
|
71
72
|
tests/test_hermes_stdio_transport.py
|
|
72
73
|
tests/test_host_events.py
|