memleaf 0.2.11__tar.gz → 0.2.13__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.11 → memleaf-0.2.13}/CHANGELOG.md +25 -0
- {memleaf-0.2.11/src/memleaf.egg-info → memleaf-0.2.13}/PKG-INFO +3 -3
- {memleaf-0.2.11 → memleaf-0.2.13}/README.en.md +2 -2
- {memleaf-0.2.11 → memleaf-0.2.13}/README.md +2 -2
- {memleaf-0.2.11 → memleaf-0.2.13}/pyproject.toml +1 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/__init__.py +1 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/hermes_provider/plugin.yaml +1 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/processing.py +174 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/prompts.py +23 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/validation.py +94 -0
- {memleaf-0.2.11 → memleaf-0.2.13/src/memleaf.egg-info}/PKG-INFO +3 -3
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_admission_noise.py +142 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_hermes_provider.py +4 -4
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_pypi_install.py +5 -5
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b1.py +81 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b2a.py +274 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_c1_mcp.py +1 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_c3_packaging.py +1 -1
- {memleaf-0.2.11 → memleaf-0.2.13}/IMPLEMENTATION_PLAN.md +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/LICENSE +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/MANIFEST.in +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/RELEASE_CHECKLIST.md +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/examples/README.md +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/examples/basic_usage.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/examples/mcp_stdio.ndjson +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/install.ps1 +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/install.sh +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/setup.cfg +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/__main__.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/adapters/__init__.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/adapters/antigravity.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/adapters/base.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/adapters/codex.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/adapters/hermes.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/budget.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/capture.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/cli.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/compaction.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/config.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/credentials.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/frontmatter.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/hermes_provider/README.md +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/hermes_provider/__init__.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/host_events.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/host_runtime.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/inbox.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/index.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/installer.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/llm/__init__.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/llm/base.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/llm/claude_compatible.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/llm/gemini.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/llm/openai_compatible.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/llm/router.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/locking.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/mcp_server.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/memory_writer.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/model_discovery.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/models.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/native_index.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/redaction.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/retrieval.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/retrieval_gate.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/scope_maintenance.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/scope_state.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/service.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf/vault.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf.egg-info/SOURCES.txt +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf.egg-info/dependency_links.txt +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf.egg-info/entry_points.txt +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/src/memleaf.egg-info/top_level.txt +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/__init__.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_codex_install.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_codex_native_cli.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_context_budget.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_credential_safety.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_cross_host_acceptance.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_hermes_stdio_transport.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_host_events.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_host_runtime_contract.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_install.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_maintenance_v2.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_model_discovery.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_retrieval_gate.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_retrieval_v2.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_session_lineage.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_a.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b2b.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3a_commit.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3a_contract.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3b_native_context.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3b_native_index.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3b_scope.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3c_retrieval.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_b3d_scope_maintenance.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_stage_c2_init.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_upgrade_preserves_vault.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_v2_gate_limits.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_v2_host_flow.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_v2_mcp_flow.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/tests/test_v2_nomatch_semantics.py +0 -0
- {memleaf-0.2.11 → memleaf-0.2.13}/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.13 — 2026-09-02
|
|
6
|
+
|
|
7
|
+
- Preserve the full active project-plan body, stable title, and retrieval
|
|
8
|
+
metadata when a customer suggestion only adds constraints; explicit
|
|
9
|
+
replacements continue to replace conflicting state normally.
|
|
10
|
+
- Reject attachment, document, presentation, material, and issue-list items
|
|
11
|
+
described only by transport metadata or a generic follow-up at both the gate
|
|
12
|
+
and summary boundaries.
|
|
13
|
+
- Keep concrete attachment-backed actions eligible when they include a
|
|
14
|
+
remediation, owner, deadline, delivery rule, deployment, or migration.
|
|
15
|
+
|
|
16
|
+
## 0.2.12 — 2026-09-02
|
|
17
|
+
|
|
18
|
+
- Remove empty ASCII or Chinese parentheses left after an absolute date is
|
|
19
|
+
normalized, so permanent memory never stores values such as
|
|
20
|
+
`2026-09-03()`.
|
|
21
|
+
- Reconcile a same-scoped implementation-plan candidate with its unique active
|
|
22
|
+
project-plan memory even when the model labels the candidate as a fact or
|
|
23
|
+
omits `update_memory_id`; preserve the target's immutable type and history.
|
|
24
|
+
- Exclude sent-mail, attachment, archive, and meeting records from automatic
|
|
25
|
+
plan-target inference, and defer instead of guessing when multiple project
|
|
26
|
+
plan targets remain.
|
|
27
|
+
- Reject combined mailbox or daily-report count/status digests as permanent
|
|
28
|
+
memory while keeping separately emitted atomic actions eligible.
|
|
29
|
+
|
|
5
30
|
## 0.2.11 — 2026-09-02
|
|
6
31
|
|
|
7
32
|
- 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.
|
|
3
|
+
Version: 0.2.13
|
|
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.13。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 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.13.**
|
|
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.13 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.13。**
|
|
8
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 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.13"
|
|
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
|
|
@@ -64,6 +64,8 @@ from .validation import (
|
|
|
64
64
|
parse_strict_json,
|
|
65
65
|
parse_summarize_output,
|
|
66
66
|
normalize_relative_calendar_text,
|
|
67
|
+
is_aggregate_operational_text,
|
|
68
|
+
is_attachment_followup_only_text,
|
|
67
69
|
)
|
|
68
70
|
from .vault import safe_component
|
|
69
71
|
|
|
@@ -1682,6 +1684,160 @@ class Processor:
|
|
|
1682
1684
|
# parser has already limited the target to an active related memory.
|
|
1683
1685
|
return candidate_scope_source in {"user", "session_context"}
|
|
1684
1686
|
|
|
1687
|
+
@staticmethod
|
|
1688
|
+
def _project_scope_keys(scopes: Any) -> set[str]:
|
|
1689
|
+
if not isinstance(scopes, list):
|
|
1690
|
+
return set()
|
|
1691
|
+
return {
|
|
1692
|
+
scope.casefold()
|
|
1693
|
+
for scope in scopes
|
|
1694
|
+
if isinstance(scope, str) and scope.startswith("project:")
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
@staticmethod
|
|
1698
|
+
def _is_project_plan_title(value: Any) -> bool:
|
|
1699
|
+
text = normalize_term(value) if isinstance(value, str) else ""
|
|
1700
|
+
return any(
|
|
1701
|
+
marker in text
|
|
1702
|
+
for marker in ("实施计划", "项目计划", "implementation plan", "project plan")
|
|
1703
|
+
)
|
|
1704
|
+
|
|
1705
|
+
@staticmethod
|
|
1706
|
+
def _is_adjacent_plan_record(value: Any) -> bool:
|
|
1707
|
+
text = normalize_term(value) if isinstance(value, str) else ""
|
|
1708
|
+
return any(
|
|
1709
|
+
marker in text
|
|
1710
|
+
for marker in (
|
|
1711
|
+
"已发送", "发送", "邮件", "附件", "存档", "会议", "启动会", "纪要",
|
|
1712
|
+
"sent", "email", "mail", "attachment", "archive", "meeting", "minutes",
|
|
1713
|
+
)
|
|
1714
|
+
)
|
|
1715
|
+
|
|
1716
|
+
@classmethod
|
|
1717
|
+
def _merge_additive_project_plan_update(
|
|
1718
|
+
cls,
|
|
1719
|
+
candidate: Mapping[str, Any],
|
|
1720
|
+
summary: Mapping[str, Any],
|
|
1721
|
+
target: Memory | None,
|
|
1722
|
+
) -> dict[str, Any]:
|
|
1723
|
+
"""Preserve a full active plan when current evidence only adds constraints."""
|
|
1724
|
+
|
|
1725
|
+
result = dict(summary)
|
|
1726
|
+
if (
|
|
1727
|
+
target is None
|
|
1728
|
+
or target.type != "project"
|
|
1729
|
+
or not cls._is_project_plan_title(target.title)
|
|
1730
|
+
):
|
|
1731
|
+
return result
|
|
1732
|
+
candidate_text = str(candidate.get("memory", "")).casefold()
|
|
1733
|
+
additive_markers = (
|
|
1734
|
+
"建议", "补充", "增加", "新增", "追加", "还需", "同时", "纳入", "完善",
|
|
1735
|
+
"suggest", "propose", "additional", "add ", "include",
|
|
1736
|
+
)
|
|
1737
|
+
replacement_markers = (
|
|
1738
|
+
"改为", "变更为", "替换", "取消", "删除", "不再", "更新为", "调整为", "推迟至", "提前至",
|
|
1739
|
+
"replace", "changed to", "no longer", "cancel", "remove", "instead", "moved to",
|
|
1740
|
+
)
|
|
1741
|
+
if not any(marker in candidate_text for marker in additive_markers):
|
|
1742
|
+
return result
|
|
1743
|
+
if any(marker in candidate_text for marker in replacement_markers):
|
|
1744
|
+
return result
|
|
1745
|
+
|
|
1746
|
+
old_body = target.body.strip()
|
|
1747
|
+
new_body = str(result.get("body", "")).strip()
|
|
1748
|
+
normalized_old = " ".join(old_body.casefold().split())
|
|
1749
|
+
normalized_new = " ".join(new_body.casefold().split())
|
|
1750
|
+
if old_body and normalized_old not in normalized_new:
|
|
1751
|
+
result["body"] = f"{old_body}\n\n{new_body}" if new_body else old_body
|
|
1752
|
+
result["title"] = target.title
|
|
1753
|
+
for field in ("tags", "aliases", "keywords"):
|
|
1754
|
+
merged: list[str] = []
|
|
1755
|
+
for value in list(getattr(target, field)) + list(result.get(field, [])):
|
|
1756
|
+
if isinstance(value, str) and value not in merged:
|
|
1757
|
+
merged.append(value)
|
|
1758
|
+
if merged or field in result:
|
|
1759
|
+
result[field] = merged
|
|
1760
|
+
return result
|
|
1761
|
+
|
|
1762
|
+
def _infer_update_target(
|
|
1763
|
+
self,
|
|
1764
|
+
candidate: Mapping[str, Any],
|
|
1765
|
+
related: Iterable[Mapping[str, Any]],
|
|
1766
|
+
) -> dict[str, Any]:
|
|
1767
|
+
"""Reuse one unambiguously matching project plan/constraint memory.
|
|
1768
|
+
|
|
1769
|
+
The gate remains authoritative when it names a target. This narrow
|
|
1770
|
+
fallback handles a common model omission: a project-plan candidate is
|
|
1771
|
+
emitted as a new ``fact`` without ``update_memory_id`` even though the
|
|
1772
|
+
same scoped plan is already active. The active target's type is the
|
|
1773
|
+
canonical type; it is copied before summary validation so the target
|
|
1774
|
+
type invariant remains enforced.
|
|
1775
|
+
"""
|
|
1776
|
+
|
|
1777
|
+
result = dict(candidate)
|
|
1778
|
+
if (
|
|
1779
|
+
not result.get("worth")
|
|
1780
|
+
or result.get("duplicate")
|
|
1781
|
+
or any(result.get(field) for field in ("duplicate_memory_id", "update_memory_id"))
|
|
1782
|
+
or not isinstance(result.get("memory"), str)
|
|
1783
|
+
):
|
|
1784
|
+
return result
|
|
1785
|
+
if result.get("type") not in {"fact", "project"}:
|
|
1786
|
+
return result
|
|
1787
|
+
if not self._is_project_plan_title(result["memory"]):
|
|
1788
|
+
return result
|
|
1789
|
+
project_keys = self._project_scope_keys(result.get("scopes"))
|
|
1790
|
+
if len(project_keys) != 1:
|
|
1791
|
+
return result
|
|
1792
|
+
|
|
1793
|
+
project_matches: list[Memory] = []
|
|
1794
|
+
fact_matches: list[Memory] = []
|
|
1795
|
+
seen: set[str] = set()
|
|
1796
|
+
for item in related:
|
|
1797
|
+
if not isinstance(item, Mapping) or item.get("native") is True:
|
|
1798
|
+
continue
|
|
1799
|
+
memory_id = item.get("memory_id")
|
|
1800
|
+
item_type = item.get("type")
|
|
1801
|
+
item_scopes = item.get("scopes")
|
|
1802
|
+
if (
|
|
1803
|
+
not isinstance(memory_id, str)
|
|
1804
|
+
or not isinstance(item_type, str)
|
|
1805
|
+
or item_type not in {"fact", "project"}
|
|
1806
|
+
or self._project_scope_keys(item_scopes) != project_keys
|
|
1807
|
+
or memory_id.casefold() in seen
|
|
1808
|
+
or not self._is_project_plan_title(item.get("title"))
|
|
1809
|
+
or self._is_adjacent_plan_record(item.get("title"))
|
|
1810
|
+
):
|
|
1811
|
+
continue
|
|
1812
|
+
try:
|
|
1813
|
+
target = Memory.from_mapping(item)
|
|
1814
|
+
except (TypeError, ValueError):
|
|
1815
|
+
continue
|
|
1816
|
+
seen.add(memory_id.casefold())
|
|
1817
|
+
if target.type == "project":
|
|
1818
|
+
project_matches.append(target)
|
|
1819
|
+
else:
|
|
1820
|
+
fact_matches.append(target)
|
|
1821
|
+
|
|
1822
|
+
# Prefer a project target over a same-topic fact. If there are
|
|
1823
|
+
# multiple project targets, defer rather than creating a sibling by
|
|
1824
|
+
# guessing; the same conservative rule applies when only multiple
|
|
1825
|
+
# durable fact targets remain.
|
|
1826
|
+
if len(project_matches) > 1 or (not project_matches and len(fact_matches) > 1):
|
|
1827
|
+
result["_defer_reason"] = "ambiguous_update_target"
|
|
1828
|
+
return result
|
|
1829
|
+
if project_matches:
|
|
1830
|
+
target = project_matches[0]
|
|
1831
|
+
elif len(fact_matches) == 1:
|
|
1832
|
+
target = fact_matches[0]
|
|
1833
|
+
else:
|
|
1834
|
+
return result
|
|
1835
|
+
result["update_memory_id"] = target.memory_id
|
|
1836
|
+
# The update target's type is immutable. Correct a model's fact-vs-
|
|
1837
|
+
# project label only after the same-use target was identified.
|
|
1838
|
+
result["type"] = target.type
|
|
1839
|
+
return result
|
|
1840
|
+
|
|
1685
1841
|
def _defer_candidate(
|
|
1686
1842
|
self,
|
|
1687
1843
|
turn_ref: tuple[str, str, str],
|
|
@@ -2026,6 +2182,13 @@ class Processor:
|
|
|
2026
2182
|
requests: list[dict[str, Any]] = []
|
|
2027
2183
|
observed_scopes: list[str] = []
|
|
2028
2184
|
for candidate in gate["candidates"]:
|
|
2185
|
+
# A combined mailbox/daily digest is not an atomic memory. If a
|
|
2186
|
+
# concrete action was worth retaining, the gate must emit it as
|
|
2187
|
+
# its own candidate; the aggregate shell itself is NO_CHANGE.
|
|
2188
|
+
if candidate.get("worth") and is_aggregate_operational_text(candidate.get("memory")):
|
|
2189
|
+
continue
|
|
2190
|
+
if candidate.get("worth") and is_attachment_followup_only_text(candidate.get("memory")):
|
|
2191
|
+
continue
|
|
2029
2192
|
candidate_scopes = list(candidate["scopes"])
|
|
2030
2193
|
# An automatic candidate with no reliable project attribution is
|
|
2031
2194
|
# retained as a retryable inbox turn, never silently promoted to
|
|
@@ -2075,6 +2238,11 @@ class Processor:
|
|
|
2075
2238
|
priority_only=scope_directory is not None,
|
|
2076
2239
|
scope_records=scoped_records if scope_directory is not None else None,
|
|
2077
2240
|
)
|
|
2241
|
+
candidate = self._infer_update_target(candidate, candidate_related)
|
|
2242
|
+
defer_reason = candidate.pop("_defer_reason", None)
|
|
2243
|
+
if defer_reason:
|
|
2244
|
+
self._defer_candidate(turn_ref, candidate, defer_reason)
|
|
2245
|
+
continue
|
|
2078
2246
|
candidate_native_ids = [item["native_id"] for item in candidate_native_refs]
|
|
2079
2247
|
candidate_memory_ids = [
|
|
2080
2248
|
item["memory_id"]
|
|
@@ -2203,6 +2371,10 @@ class Processor:
|
|
|
2203
2371
|
continue
|
|
2204
2372
|
if summary.get("decision") == NO_CHANGE_DECISION:
|
|
2205
2373
|
continue
|
|
2374
|
+
if is_attachment_followup_only_text(
|
|
2375
|
+
f"{summary.get('title', '')}\n{summary.get('body', '')}"
|
|
2376
|
+
):
|
|
2377
|
+
continue
|
|
2206
2378
|
if gate_update_target is not None:
|
|
2207
2379
|
summary_update_target = summary.get("update_memory_id")
|
|
2208
2380
|
if summary_update_target is None:
|
|
@@ -2219,6 +2391,8 @@ class Processor:
|
|
|
2219
2391
|
else:
|
|
2220
2392
|
summary = dict(summary)
|
|
2221
2393
|
summary["update_memory_id"] = gate_update_target
|
|
2394
|
+
target = self.service.read(gate_update_target, include_history=False)
|
|
2395
|
+
summary = self._merge_additive_project_plan_update(candidate, summary, target)
|
|
2222
2396
|
if summary["scopes"] == ["unscoped"] or summary.get("scope_source") == "insufficient_context":
|
|
2223
2397
|
self._defer_candidate(
|
|
2224
2398
|
turn_ref,
|
|
@@ -115,6 +115,10 @@ independent future use, set worth=false. A one-off meeting with no durable
|
|
|
115
115
|
decision or commitment, a request awaiting PM acceptance, and pending feedback
|
|
116
116
|
or another intermediate status normally have worth=false. Keep an independent
|
|
117
117
|
project risk or concrete future action even when it came from a digest.
|
|
118
|
+
A PPT, attachment, material, document, or issue-list item described only as
|
|
119
|
+
"needs follow-up/review/processing" has no independent action and normally has
|
|
120
|
+
worth=false; keep it only when the evidence supplies a concrete remediation,
|
|
121
|
+
owner, deadline, delivery rule, migration, or durable conclusion.
|
|
118
122
|
An intermediate state such as a draft awaiting confirmation, preparation/
|
|
119
123
|
processing status, temporary error, repeated confirmation, or unconfirmed
|
|
120
124
|
suggestion normally has no independent future use and is worth=false. Keep one
|
|
@@ -141,6 +145,16 @@ the current turn adds no confirmed change, use worth=false and do not create a
|
|
|
141
145
|
sibling. If a customer or user proposes a new change without confirmation of
|
|
142
146
|
implementation, preserve it as requested/proposed/pending and never state that
|
|
143
147
|
it is already implemented.
|
|
148
|
+
When a candidate names a project plan or project constraint already represented
|
|
149
|
+
by one same-scoped active memory, prefer that exact active memory as
|
|
150
|
+
update_memory_id, even when the candidate's initial type label is fact; the
|
|
151
|
+
active target type is immutable and must be copied. Never create a sibling for
|
|
152
|
+
the same future question/action. Prefer the durable project-plan target over
|
|
153
|
+
same-topic sent-mail, attachment/archive, or meeting records; if multiple
|
|
154
|
+
same-use project targets remain, do not guess an update target. A candidate
|
|
155
|
+
that still combines a mailbox or daily digest shell with counts/statuses is not
|
|
156
|
+
atomic: set worth=false for the aggregate, and emit any independent concrete
|
|
157
|
+
action as a separate candidate.
|
|
144
158
|
Return no prose, markdown fences, comments, or trailing text."""
|
|
145
159
|
|
|
146
160
|
|
|
@@ -172,7 +186,8 @@ still has a concrete future-use fact or action. If not, return exactly
|
|
|
172
186
|
{"decision":"NO_CHANGE"}; this is the only no-write summary response. Use it
|
|
173
187
|
for temporary delegation, waiting for feedback, a one-off meeting arrangement with
|
|
174
188
|
no durable outcome/decision/project constraint, or a subject/attachment-only item
|
|
175
|
-
without a concrete impact or conclusion.
|
|
189
|
+
without a concrete impact or conclusion. Attachment sizes, mail/message IDs,
|
|
190
|
+
and a generic "needs follow-up" phrase do not create such an impact.
|
|
176
191
|
Explicit remember mode never permits NO_CHANGE and must return the normal summary
|
|
177
192
|
object. When producing a normal automatic summary, copy the gate candidate's
|
|
178
193
|
scopes exactly. scope_source may be omitted to inherit the gate value, but an
|
|
@@ -243,6 +258,13 @@ surrounding diagnosis into a memory. If a user-confirmed future-use
|
|
|
243
258
|
preference, identity, constraint, project risk, or durable lesson appears in
|
|
244
259
|
the same evidence, summarize only that future-use topic. In explicit remember mode,
|
|
245
260
|
summarize only the requested object and do not append tool/test diagnostics.
|
|
261
|
+
If the gate candidate points to a same-scoped existing plan or constraint,
|
|
262
|
+
update that exact target and retain its still-valid content; do not create a
|
|
263
|
+
new sibling merely because the gate called it a fact. Prefer a durable project
|
|
264
|
+
plan target over same-topic sent-mail, attachment/archive, or meeting records;
|
|
265
|
+
the target type remains immutable. Never summarize a combined mailbox/daily
|
|
266
|
+
digest shell with counts or statuses; return NO_CHANGE for that aggregate,
|
|
267
|
+
while a separate atomic future-use action may still be summarized.
|
|
246
268
|
Return no prose, markdown fences, comments, or trailing text."""
|
|
247
269
|
|
|
248
270
|
|
|
@@ -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,82 @@ 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
|
+
|
|
398
|
+
_ATTACHMENT_SUBJECT_MARKERS = (
|
|
399
|
+
"附件", "材料", "文档", "问题清单", "ppt", "slides", "deck", "attachment", "checklist",
|
|
400
|
+
)
|
|
401
|
+
_GENERIC_FOLLOWUP_MARKERS = (
|
|
402
|
+
"待跟进", "需跟进", "需要跟进", "待处理", "需处理", "需要处理", "请查看", "看下",
|
|
403
|
+
"follow up", "follow-up", "needs review", "to review",
|
|
404
|
+
)
|
|
405
|
+
_CONCRETE_ATTACHMENT_ACTION_MARKERS = (
|
|
406
|
+
"迁移", "部署", "修复", "整改", "逐项", "发送给", "发给", "抄送", "提交", "回复", "确认",
|
|
407
|
+
"负责人", "禁止", "必须", "以后", "归档至", "保存至", "migrate", "deploy", "fix",
|
|
408
|
+
"remediate", "send to", "cc ", "owner", "must",
|
|
409
|
+
)
|
|
410
|
+
_TRANSPORT_DETAIL = re.compile(
|
|
411
|
+
r"(?:\d+(?:\.\d+)?\s*(?:kb|mb|gb|字节)|(?:邮件|mail|message)\s*(?:id\s*[=::]?\s*)?#?\d+)",
|
|
412
|
+
re.IGNORECASE,
|
|
413
|
+
)
|
|
414
|
+
_CONCRETE_DEADLINE = re.compile(
|
|
415
|
+
r"(?:截至|截止|在|by|before)\s*\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\s*(?:前|之前|截止)",
|
|
416
|
+
re.IGNORECASE,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def is_attachment_followup_only_text(value: Any) -> bool:
|
|
421
|
+
"""Return true for an attachment subject with no reusable concrete action."""
|
|
422
|
+
|
|
423
|
+
if not isinstance(value, str):
|
|
424
|
+
return False
|
|
425
|
+
folded = value.casefold()
|
|
426
|
+
if not any(marker in folded for marker in _ATTACHMENT_SUBJECT_MARKERS):
|
|
427
|
+
return False
|
|
428
|
+
if any(marker in folded for marker in _CONCRETE_ATTACHMENT_ACTION_MARKERS):
|
|
429
|
+
return False
|
|
430
|
+
if _CONCRETE_DEADLINE.search(value):
|
|
431
|
+
return False
|
|
432
|
+
return any(marker in folded for marker in _GENERIC_FOLLOWUP_MARKERS) or bool(
|
|
433
|
+
_TRANSPORT_DETAIL.search(value)
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
|
|
348
437
|
def _reject_constant(value: str) -> None:
|
|
349
438
|
raise ModelOutputError("non-finite JSON number is not allowed", validation_reason="invalid_json")
|
|
350
439
|
|
|
@@ -1022,6 +1111,9 @@ def validate_summarize_output(
|
|
|
1022
1111
|
}
|
|
1023
1112
|
_require_keys(value, {"title", "body", "tags", "type", "scopes", "sources"}, allowed)
|
|
1024
1113
|
item = dict(value)
|
|
1114
|
+
for field in ("title", "body"):
|
|
1115
|
+
if isinstance(item.get(field), str):
|
|
1116
|
+
item[field] = _strip_empty_iso_date_parenthesis(item[field])
|
|
1025
1117
|
_string(item["title"], "title")
|
|
1026
1118
|
_string(item["body"], "body", multiline=True)
|
|
1027
1119
|
_reject_relative_calendar_expression(item)
|
|
@@ -1281,6 +1373,8 @@ __all__ = [
|
|
|
1281
1373
|
"parse_gate_output",
|
|
1282
1374
|
"parse_compact_output",
|
|
1283
1375
|
"parse_strict_json",
|
|
1376
|
+
"is_aggregate_operational_text",
|
|
1377
|
+
"is_attachment_followup_only_text",
|
|
1284
1378
|
"normalize_relative_calendar_text",
|
|
1285
1379
|
"parse_summarize_output",
|
|
1286
1380
|
"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.13
|
|
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.13。**
|
|
27
|
+
> 核心库、Vault、stdio MCP Server、初始化 CLI、模型路由、提炼流程、受控检索协议和宿主适配器已经实现。memleaf 0.2.13 通过 PyPI 分发。
|
|
28
28
|
> **当前版本支持 Hermes 和 Codex。** Antigravity(反重力)不检测、不安装、不配置。
|
|
29
29
|
|
|
30
30
|
## 项目定位
|